phronomy 0.22.0 → 0.23.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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +200 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +152 -0
  15. data/docs/architecture/persistence.md +175 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +82 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/README.md +172 -0
  45. data/docs/features.md +31 -11
  46. data/docs/getting-started.md +77 -45
  47. data/docs/migrations/0.19.md +14 -7
  48. data/docs/migrations/0.22.md +390 -0
  49. data/docs/persistence-backends.md +88 -38
  50. data/docs/runtime-and-concurrency.md +227 -33
  51. data/examples/README.md +13 -0
  52. data/lib/phronomy/agent/agent_execution.rb +19 -15
  53. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  54. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  55. data/lib/phronomy/agent/agent_root.rb +3 -3
  56. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  57. data/lib/phronomy/agent/async_event_api.rb +145 -72
  58. data/lib/phronomy/agent/base.rb +388 -181
  59. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  60. data/lib/phronomy/agent/context_assembler.rb +437 -178
  61. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  62. data/lib/phronomy/agent/context_plan.rb +18 -13
  63. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  64. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  65. data/lib/phronomy/agent/context_policy.rb +109 -3
  66. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  67. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  68. data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
  69. data/lib/phronomy/agent/journal_record.rb +17 -4
  70. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  71. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  72. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  73. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  74. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  75. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
  76. data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
  77. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
  78. data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
  79. data/lib/phronomy/agent/recovery_support.rb +512 -0
  80. data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
  81. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  82. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  83. data/lib/phronomy/agent/shared_state.rb +38 -1
  84. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  85. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  86. data/lib/phronomy/agent/tool_invocation.rb +336 -102
  87. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  88. data/lib/phronomy/agent.rb +20 -2
  89. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  90. data/lib/phronomy/agent_purged_error.rb +5 -0
  91. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  92. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  93. data/lib/phronomy/engine/event_loop.rb +622 -63
  94. data/lib/phronomy/engine/fsm_session.rb +194 -21
  95. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  96. data/lib/phronomy/engine/runtime.rb +77 -20
  97. data/lib/phronomy/generator_verifier.rb +12 -14
  98. data/lib/phronomy/invocation_context.rb +9 -29
  99. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  100. data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
  101. data/lib/phronomy/multi_agent/coordinator.rb +154 -0
  102. data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
  103. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  104. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  105. data/lib/phronomy/multi_agent/handoff.rb +24 -45
  106. data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
  107. data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
  108. data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
  109. data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
  110. data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
  111. data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
  112. data/lib/phronomy/multi_agent/runner.rb +98 -0
  113. data/lib/phronomy/persistence/durable_codec.rb +646 -0
  114. data/lib/phronomy/persistence/durable_record.rb +117 -0
  115. data/lib/phronomy/persistence/in_memory.rb +210 -134
  116. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  117. data/lib/phronomy/persistence/repository_facades.rb +316 -0
  118. data/lib/phronomy/persistence.rb +81 -41
  119. data/lib/phronomy/recovery.rb +186 -0
  120. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  121. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
  122. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  123. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  124. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  125. data/lib/phronomy/tracing/automatic.rb +176 -0
  126. data/lib/phronomy/tracing/base.rb +11 -2
  127. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  128. data/lib/phronomy/version.rb +1 -1
  129. data/lib/phronomy/workflow.rb +3 -6
  130. data/lib/phronomy/workflow_context.rb +14 -5
  131. data/lib/phronomy/workflow_recovery.rb +123 -0
  132. data/lib/phronomy/workflow_runner.rb +468 -256
  133. data/lib/phronomy.rb +6 -0
  134. data/scripts/api_snapshot.rb +12 -0
  135. data/sig/phronomy/agent.rbs +209 -7
  136. data/sig/phronomy/multi_agent.rbs +39 -0
  137. data/sig/phronomy/persistence.rbs +62 -4
  138. data/sig/phronomy/runtime.rbs +1 -4
  139. data/sig/phronomy/workflow.rbs +2 -2
  140. data/sig/phronomy.rbs +10 -0
  141. metadata +65 -17
  142. data/examples/workflows/agent_event_mapping.rb +0 -101
  143. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  144. data/lib/phronomy/agent/activation_registry.rb +0 -28
  145. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  146. data/lib/phronomy/agent/context_candidate.rb +0 -47
  147. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  148. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  149. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  150. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  151. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  152. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  153. data/lib/phronomy/agent/context_request.rb +0 -35
  154. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  155. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  156. data/lib/phronomy/agent/runner.rb +0 -97
@@ -0,0 +1,507 @@
1
+ > **ARCHIVED / non-normative**
2
+ >
3
+ > This removed/obsolete design is retained only as historical reference. It is
4
+ > not a current architecture or public-API contract. Start at
5
+ > [docs/architecture.md](../../../architecture.md) and the
6
+ > [ADR index](../../../decisions/README.md).
7
+
8
+ # Phronomy — Public API Design
9
+
10
+ > **ARCHIVED — Early Design Draft**
11
+ >
12
+ > This file reflects the intended API as of the initial design phase and has
13
+ > not been updated to track the released implementation. Many examples below
14
+ > use APIs that have been renamed, removed, or changed since this document
15
+ > was written. For the current public API, see [README.md](../../../../README.md).
16
+ >
17
+ > Known divergences (non-exhaustive):
18
+ > - `after :state, to: :next` DSL → current DSL: `transition from: :state, to: :next`
19
+ > - `event :name, from:, to:` DSL → current DSL: `transition from:, on: :name, to:`
20
+ > - `app.send_event(:event, config: { thread_id: })` → current: `app.send_event(state:, event:)`
21
+ > - `Phronomy.chain(...)` / `Phronomy.workflow(...)` shortcut methods → removed
22
+ > - `config.default_state_store`, `config.default_memory` → not implemented
23
+ > - `Phronomy::StateStore::*`, `Phronomy::Memory::WindowMemory` → not in gem
24
+ > - `app.stream(input, config:) { |event| }` → not in current public API
25
+
26
+ ## 1. Gem Entry Point
27
+
28
+ ```ruby
29
+ # lib/phronomy.rb
30
+
31
+ require "ruby_llm"
32
+ require "zeitwerk"
33
+
34
+ loader = Zeitwerk::Loader.for_gem
35
+ loader.setup
36
+
37
+ module Phronomy
38
+ class Error < StandardError; end
39
+ class ParseError < Error; end
40
+ class RecursionLimitError < Error; end
41
+ class GuardrailError < Error; end
42
+ class ToolError < Error
43
+ attr_reader :tool_name, :cause
44
+ def initialize(tool_name:, cause:)
45
+ @tool_name = tool_name
46
+ @cause = cause
47
+ super("Tool #{tool_name} failed: #{cause}")
48
+ end
49
+ end
50
+
51
+ class << self
52
+ def configuration
53
+ @configuration ||= Configuration.new
54
+ end
55
+
56
+ def configure
57
+ yield configuration
58
+ end
59
+
60
+ # Shortcut: returns a Chain
61
+ # @example
62
+ # Phronomy.chain(model: "gpt-4o") do |c|
63
+ # c.prompt "Answer: <%= question %>"
64
+ # c.parse :json
65
+ # end
66
+ def chain(**opts, &block)
67
+ Chain::Builder.new(**opts).tap { |b| block.call(b) if block }.build
68
+ end
69
+
70
+ # Shortcut: build a Workflow
71
+ # @example
72
+ # app = Phronomy::Workflow.define(MyContext) do
73
+ # initial :fetch
74
+ # state :fetch, action: FETCH_FN
75
+ # end
76
+ def workflow(context_class, &block)
77
+ Workflow.define(context_class, &block)
78
+ end
79
+ end
80
+ end
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 2. Configuration API
86
+
87
+ ```ruby
88
+ Phronomy.configure do |config|
89
+ # Default model (shared with RubyLLM configuration)
90
+ config.default_model = "claude-3-5-sonnet-20241022"
91
+ config.default_embedding_model = "text-embedding-3-small"
92
+
93
+ # Default checkpointer
94
+ config.default_state_store = Phronomy::StateStore::InMemory.new
95
+
96
+ # Default memory
97
+ config.default_memory = Phronomy::Memory::WindowMemory.new(k: 20)
98
+
99
+ # Default tracer
100
+ config.tracer = Phronomy::Tracing::NullTracer.new
101
+
102
+ # Recursion limit
103
+ config.recursion_limit = 25
104
+
105
+ # Default Human-in-the-Loop behavior
106
+ config.interrupt_handler = nil # Set a Proc for default handling
107
+ end
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 3. Chain API
113
+
114
+ ```ruby
115
+ # === Pattern A: Pipeline via >> operator ===
116
+ prompt = Phronomy::Chain::PromptTemplate.new(
117
+ template: "Please answer the question: <%= question %>",
118
+ system_template: "You are a helpful assistant."
119
+ )
120
+ llm = Phronomy::Chain::LLMChain.new(model: "gpt-4o")
121
+ parser = Phronomy::OutputParser::JsonParser.new
122
+
123
+ chain = prompt >> llm >> parser
124
+ result = chain.invoke(question: "What is Ruby?")
125
+
126
+ # === Pattern B: Builder DSL ===
127
+ chain = Phronomy.chain(model: "claude-3-5-sonnet") do |c|
128
+ c.system "You are an assistant that replies in JSON only."
129
+ c.prompt "Answer the following question in JSON format: <%= question %>"
130
+ c.parse :json
131
+ end
132
+
133
+ # === Pattern C: File-based template ===
134
+ prompt = Phronomy::Chain::PromptTemplate.from_file(
135
+ "prompts/research.txt",
136
+ system_path: "prompts/system.txt"
137
+ )
138
+
139
+ # === Streaming ===
140
+ chain.stream(question: "What are the new features in Ruby 3.4?") do |chunk|
141
+ print chunk
142
+ $stdout.flush
143
+ end
144
+
145
+ # === Batch execution ===
146
+ results = chain.batch([
147
+ { question: "What is Ruby?" },
148
+ { question: "What is Rails?" },
149
+ { question: "What is RubyGems?" }
150
+ ])
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 4. Workflow API
156
+
157
+ > **STALE DSL NOTE**: `after :state, to: :next` and `event :name, from:, to:`
158
+ > shown below do not exist in the gem. Use `transition from:, to:` (auto-fire)
159
+ > and `transition from:, on: :name, to:` (external event) instead.
160
+ > `app.send_event(:event, config:)` should be `app.send_event(state:, event:)`.
161
+
162
+ ```ruby
163
+ # === Context definition ===
164
+ class MyWorkflowContext
165
+ include Phronomy::WorkflowContext
166
+
167
+ field :input, type: :replace
168
+ field :messages, type: :append, default: -> { [] }
169
+ field :result, type: :replace
170
+ field :done, type: :replace, default: false
171
+ end
172
+
173
+ # === Workflow definition ===
174
+ app = Phronomy::Workflow.define(MyWorkflowContext) do
175
+ initial :fetch
176
+
177
+ state :fetch, action: ->(s) {
178
+ data = fetch_data(s.input)
179
+ s.merge(messages: [{ role: :user, content: data }])
180
+ }
181
+
182
+ state :process, action: ->(s) {
183
+ response = RubyLLM.chat.ask(s.messages.last[:content])
184
+ s.merge(result: response.content, done: true)
185
+ }
186
+
187
+ state :retry_handler, action: ->(s) {
188
+ s.merge(messages: [{ role: :user, content: "Please retry." }])
189
+ }
190
+
191
+ after :fetch, to: :process
192
+ # Conditional: retry if not done, else finish
193
+ after :process, to: :retry_handler, guard: ->(s) { !s.done }
194
+ after :process, to: :__finish__
195
+ after :retry_handler, to: :process
196
+ end
197
+
198
+ # === Execute ===
199
+ Phronomy.configure { |c| c.default_state_store = Phronomy::StateStore::ActiveRecord.new(...) }
200
+
201
+ result = app.invoke(
202
+ { input: "Data to analyze" },
203
+ config: { thread_id: "session_#{user.id}" }
204
+ )
205
+
206
+ # === Human-in-the-Loop with wait_state ===
207
+ class ApprovalContext
208
+ include Phronomy::WorkflowContext
209
+ field :input, type: :replace
210
+ field :result, type: :replace
211
+ end
212
+
213
+ app = Phronomy::Workflow.define(ApprovalContext) do
214
+ initial :process
215
+ state :process, action: PROCESS_NODE
216
+ wait_state :awaiting_approval # execution halts here
217
+ state :finalize, action: FINALIZE_NODE
218
+ after :process, to: :awaiting_approval
219
+ after :finalize, to: :__finish__
220
+ event :approve, from: :awaiting_approval, to: :finalize
221
+ end
222
+
223
+ # First invocation halts at :awaiting_approval
224
+ state = app.invoke({ input: "..." }, config: { thread_id: "t1" })
225
+ puts "Halted: #{state.halted?}, phase: #{state.phase}"
226
+
227
+ # Human approves — resume via send_event
228
+ app.send_event(:approve, config: { thread_id: "t1" })
229
+
230
+ # === Streaming ===
231
+ app.stream({ input: "..." }, config: { thread_id: "t1" }) do |event|
232
+ case event[:type]
233
+ when :node_start
234
+ puts "-> Executing #{event[:node]}..."
235
+ when :node_end
236
+ puts "\u2713 #{event[:node]} completed"
237
+ when :graph_end
238
+ puts "Workflow execution complete"
239
+ end
240
+ end
241
+ ```
242
+
243
+ ---
244
+
245
+ ## 5. Agent API
246
+
247
+ ```ruby
248
+ # === Declarative definition (class-based) ===
249
+ class CustomerSupportAgent < Phronomy::Agent::Base
250
+ model "claude-3-5-sonnet-20241022"
251
+ temperature 0.5
252
+ max_iterations 15
253
+
254
+ instructions <<~INST
255
+ You are a friendly customer support agent.
256
+ Please resolve user issues politely.
257
+ If you cannot resolve the issue, use the escalation tool.
258
+ INST
259
+
260
+ tools SearchKnowledgeBase,
261
+ CreateTicket,
262
+ EscalateToHuman
263
+ end
264
+
265
+ # === Usage ===
266
+ agent = CustomerSupportAgent.new
267
+ result = agent.invoke("What is the status of order #12345?")
268
+ puts result[:output]
269
+
270
+ # === Use as a Workflow state action ===
271
+ class SupportContext
272
+ include Phronomy::WorkflowContext
273
+ field :messages, type: :append, default: -> { [] }
274
+ end
275
+
276
+ app = Phronomy::Workflow.define(SupportContext) do
277
+ initial :greet
278
+ state :greet, action: ->(s) { s.merge(messages: ["Hello! How can I help you?"]) }
279
+ state :support, action: ->(s) { s.merge(messages: [CustomerSupportAgent.new.invoke(s.messages.last)[:output]]) }
280
+ after :greet, to: :support
281
+ after :support, to: :__finish__
282
+ end
283
+ ```
284
+
285
+ ---
286
+
287
+ ## 6. Tool API
288
+
289
+ ```ruby
290
+ # === Tool definition (inherits from RubyLLM::Tool) ===
291
+ class SearchKnowledgeBase < Phronomy::Tool::Base
292
+ description "Searches the internal knowledge base"
293
+
294
+ param :query, type: :string, desc: "Search query"
295
+ param :limit, type: :integer, desc: "Maximum results", required: false
296
+ param :category, type: :string, desc: "Category filter", required: false
297
+
298
+ # Permission scope definition (optional)
299
+ scope :read_only
300
+
301
+ # Default behavior on failure
302
+ on_error :return_empty
303
+
304
+ def execute(query:, limit: 5, category: nil)
305
+ results = KnowledgeBase.search(query:, limit:, category:)
306
+ results.map { |r| { title: r.title, content: r.body } }
307
+ end
308
+ end
309
+
310
+ # === Tool requiring human approval ===
311
+ class DeleteCustomerData < Phronomy::Tool::Base
312
+ description "Deletes customer data (requires approval)"
313
+
314
+ param :customer_id, type: :string, desc: "Customer ID"
315
+
316
+ # This tool requires human approval before execution
317
+ requires_approval true
318
+
319
+ def execute(customer_id:)
320
+ Customer.find(customer_id).destroy
321
+ { success: true, message: "Customer data deleted" }
322
+ end
323
+ end
324
+
325
+ # === MCP tool (via external MCP server) ===
326
+ mcp_tool = Phronomy::Tool::McpTool.from_server(
327
+ "stdio://path/to/mcp-server",
328
+ tool_name: "search_web"
329
+ )
330
+ ```
331
+
332
+ ---
333
+
334
+ ## 7. Memory API
335
+
336
+ ```ruby
337
+ # === Basic usage ===
338
+ memory = Phronomy::Memory::WindowMemory.new(k: 20)
339
+ memory.save_messages(
340
+ thread_id: "user_123",
341
+ messages: chat.messages
342
+ )
343
+ past_messages = memory.load_messages(thread_id: "user_123")
344
+
345
+ # === Integration with Agent / Chain ===
346
+ agent = MyAgent.new
347
+ result = agent.invoke(
348
+ "A follow-up question",
349
+ config: {
350
+ thread_id: "user_123",
351
+ memory: Phronomy::Memory::SummaryMemory.new(max_tokens: 4000)
352
+ }
353
+ )
354
+
355
+ # === Rails ActiveRecord integration ===
356
+ # config/initializers/phronomy.rb
357
+ Phronomy.configure do |c|
358
+ c.default_memory = Phronomy::Memory::ActiveRecordMemory.new
359
+ end
360
+ ```
361
+
362
+ ---
363
+
364
+ ## 8. StateStore API
365
+
366
+ ```ruby
367
+ # === In-memory (development / testing) ===
368
+ Phronomy.configure { |c| c.default_state_store = Phronomy::StateStore::InMemory.new }
369
+
370
+ # === ActiveRecord (Rails production) ===
371
+ Phronomy.configure { |c| c.default_state_store = Phronomy::StateStore::ActiveRecord.new(model_class: PhronmyState) }
372
+
373
+ # === Redis ===
374
+ Phronomy.configure { |c| c.default_state_store = Phronomy::StateStore::Redis.new(client: Redis.new, ttl: 3600) }
375
+
376
+ # === Manual operations ===
377
+ store = Phronomy::StateStore::InMemory.new
378
+ store.save(state) # state.thread_id is used as key
379
+ state = store.load("t1")
380
+ store.clear("t1")
381
+ ```
382
+
383
+ ---
384
+
385
+ ## 9. Multi-Agent (Agent-as-Tool Pattern)
386
+
387
+ ```ruby
388
+ # Sub-agents wrapped as tools so the orchestrator LLM can call them on demand.
389
+
390
+ class ResearchTool < Phronomy::Tool::Base
391
+ description "Research a topic and return key findings as bullet points."
392
+ param :topic, type: :string, desc: "The topic to research"
393
+
394
+ def execute(topic:)
395
+ ResearcherAgent.new.invoke(topic)[:output]
396
+ end
397
+ end
398
+
399
+ class WriteTool < Phronomy::Tool::Base
400
+ description "Write a technical blog post given research notes and a writing brief."
401
+ param :instructions, type: :string, desc: "Writing brief including research notes"
402
+
403
+ def execute(instructions:)
404
+ WriterAgent.new.invoke(instructions)[:output]
405
+ end
406
+ end
407
+
408
+ class OrchestratorAgent < Phronomy::Agent::Base
409
+ model "claude-3-5-sonnet-20241022"
410
+ tools ResearchTool, WriteTool
411
+ instructions "Produce a high-quality blog post. Use research tool first, then write tool."
412
+ end
413
+
414
+ result = OrchestratorAgent.new.invoke("Write a blog post about Ruby 3.4 new features")
415
+ puts result[:output]
416
+ ```
417
+
418
+ ---
419
+
420
+ ## 10. Rails Integration API
421
+
422
+ > **STALE NOTE**: `config.default_state_store`, `config.default_memory`,
423
+ > `acts_as_phronomy_thread`, and `agent.stream(...)` shown below are not
424
+ > implemented in the current gem. See README.md for the Rails example that
425
+ > ships with phronomy-examples.
426
+
427
+ ```ruby
428
+ # Gemfile
429
+ gem 'phronomy'
430
+
431
+ # config/initializers/phronomy.rb
432
+ Phronomy.configure do |config|
433
+ config.default_model = "claude-3-5-sonnet-20241022"
434
+ config.default_state_store = Phronomy::StateStore::ActiveRecord.new(model_class: PhronmyState)
435
+ config.default_memory = Phronomy::Memory::ActiveRecordMemory.new
436
+ end
437
+
438
+ # migration (generated by rails generate phronomy:install)
439
+ # create_table :phronomy_states
440
+ # create_table :phronomy_messages
441
+
442
+ # ActiveRecord model integration
443
+ class Conversation < ApplicationRecord
444
+ acts_as_phronomy_thread # use the record's id as thread_id
445
+ end
446
+
447
+ # Usage in a controller
448
+ class ChatController < ApplicationController
449
+ def create
450
+ conversation = Conversation.find(params[:id])
451
+ agent = CustomerSupportAgent.new
452
+
453
+ result = agent.invoke(
454
+ params[:message],
455
+ config: { thread_id: conversation.id.to_s }
456
+ )
457
+
458
+ render json: { response: result[:output] }
459
+ end
460
+ end
461
+
462
+ # Streaming with ActionCable
463
+ class ChatChannel < ApplicationCable::Channel
464
+ def receive(data)
465
+ agent = CustomerSupportAgent.new
466
+
467
+ agent.stream(data["message"], config: { thread_id: current_user.id.to_s }) do |chunk|
468
+ ActionCable.server.broadcast("chat_#{current_user.id}", { chunk: })
469
+ end
470
+ end
471
+ end
472
+ ```
473
+
474
+ ---
475
+
476
+ ## 11. Error Handling
477
+
478
+ ```ruby
479
+ # Standard exception hierarchy
480
+ Phronomy::Error
481
+ ├── Phronomy::ParseError # OutputParser failure
482
+ ├── Phronomy::RecursionLimitError # Workflow recursion limit exceeded
483
+ ├── Phronomy::GuardrailError # Guardrail violation
484
+ └── Phronomy::ToolError # Tool execution failure
485
+ attr_reader :tool_name, :cause
486
+
487
+ # Halt and resume (no exception raised — use wait_state + send_event)
488
+ app = Phronomy::Workflow.define(MyContext) do
489
+ # ...
490
+ wait_state :awaiting_approval
491
+ event :approve, from: :awaiting_approval, to: :next_step
492
+ end
493
+
494
+ state = app.invoke(input, config: { thread_id: "t1" })
495
+ if state.halted?
496
+ # User reviews state, then resumes
497
+ app.send_event(:approve, config: { thread_id: "t1" })
498
+ end
499
+
500
+ rescue Phronomy::RecursionLimitError
501
+ Rails.logger.error "Workflow recursion limit exceeded"
502
+ rescue Phronomy::ToolError => e
503
+ Rails.logger.error "Tool #{e.tool_name} failed: #{e.cause}"
504
+ end
505
+ ```
506
+
507
+ ---
@@ -0,0 +1,186 @@
1
+ > **ARCHIVED / non-normative**
2
+ >
3
+ > This removed/obsolete design is retained only as historical reference. It is
4
+ > not a current architecture or public-API contract. Start at
5
+ > [docs/architecture.md](../../../architecture.md) and the
6
+ > [ADR index](../../../decisions/README.md).
7
+
8
+ # Phronomy — Guardrail System
9
+
10
+ ## 1. Overview
11
+
12
+ Guardrails are validation hooks that intercept agent inputs and outputs before
13
+ they reach (or leave) the LLM. They provide a clean interception point for
14
+ safety, compliance, and data-quality concerns without modifying core agent logic.
15
+
16
+ ```
17
+ User input
18
+
19
+ [InputGuardrail x N] ← raises GuardrailError on failure
20
+
21
+ LLM
22
+
23
+ [OutputGuardrail x N] ← raises GuardrailError on failure
24
+
25
+ Agent result
26
+ ```
27
+
28
+ Multiple guardrails of each type may be registered per agent and are executed
29
+ in registration order. The first failure short-circuits the remaining guardrails.
30
+
31
+ ---
32
+
33
+ ## 2. Base Class
34
+
35
+ `lib/phronomy/guardrail/base.rb`
36
+
37
+ ```ruby
38
+ class Phronomy::Guardrail::Base
39
+ def check(value) # subclasses implement this
40
+ def run!(value) # calls check; returns value unchanged on success
41
+ protected def fail!(reason) # raises Phronomy::GuardrailError
42
+ end
43
+ ```
44
+
45
+ `GuardrailError` is a standard Ruby exception. It carries:
46
+ - `message` — the human-readable rejection reason
47
+ - `guardrail` — the guardrail instance that raised
48
+
49
+ ---
50
+
51
+ ## 3. InputGuardrail
52
+
53
+ `lib/phronomy/guardrail/input_guardrail.rb`
54
+
55
+ Applied to user input **before** the LLM receives it.
56
+
57
+ ```ruby
58
+ class NoCreditCardGuardrail < Phronomy::Guardrail::InputGuardrail
59
+ def check(input)
60
+ fail!("Credit card numbers are not allowed") if input.to_s.match?(/\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}/)
61
+ end
62
+ end
63
+
64
+ agent.add_input_guardrail(NoCreditCardGuardrail.new)
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 4. OutputGuardrail
70
+
71
+ `lib/phronomy/guardrail/output_guardrail.rb`
72
+
73
+ Applied to the LLM's output **before** it is returned to the caller.
74
+
75
+ ```ruby
76
+ class NoURLGuardrail < Phronomy::Guardrail::OutputGuardrail
77
+ def check(output)
78
+ fail!("Output must not contain URLs") if output.to_s.match?(/https?:\/\//)
79
+ end
80
+ end
81
+
82
+ agent.add_output_guardrail(NoURLGuardrail.new)
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 5. Registration API
88
+
89
+ Guardrails are registered on agent instances or via the class-level DSL:
90
+
91
+ ```ruby
92
+ # Instance registration
93
+ agent = MyAgent.new
94
+ agent.add_input_guardrail(PIIPatternDetector.new)
95
+ agent.add_output_guardrail(NoURLGuardrail.new)
96
+
97
+ # Class-level DSL (applied to all instances)
98
+ class MyAgent < Phronomy::Agent::Base
99
+ input_guardrail Phronomy::Guardrail::Builtin::PIIPatternDetector.new
100
+ output_guardrail Phronomy::Guardrail::Builtin::PromptInjectionDetector.new
101
+ end
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 6. Built-in Guardrails
107
+
108
+ `lib/phronomy/guardrail/builtin/`
109
+
110
+ ### 6.1 PIIPatternDetector
111
+
112
+ `Phronomy::Guardrail::Builtin::PIIPatternDetector`
113
+
114
+ Detects common PII patterns in the input string via regex (no LLM call).
115
+
116
+ **Categories** (all active by default, each individually toggleable):
117
+
118
+ | Key | Pattern | Description |
119
+ |-----|---------|-------------|
120
+ | `:ssn` | `\d{3}-\d{2}-\d{4}` (hyphens required) | US Social Security Number |
121
+ | `:credit_card` | 16 digits optionally space/hyphen separated | Credit/debit card |
122
+ | `:email` | RFC 5322 simplified | Email address |
123
+ | `:phone` | 3-digit area code + 3–4-digit exchange + 4-digit subscriber; optional E.164 prefix | Phone number |
124
+
125
+ ```ruby
126
+ # All categories (default)
127
+ Phronomy::Guardrail::Builtin::PIIPatternDetector.new
128
+
129
+ # Only email and credit card
130
+ Phronomy::Guardrail::Builtin::PIIPatternDetector.new(
131
+ detect: [:email, :credit_card]
132
+ )
133
+ ```
134
+
135
+ ### 6.2 PromptInjectionDetector
136
+
137
+ `Phronomy::Guardrail::Builtin::PromptInjectionDetector`
138
+
139
+ Detects common prompt injection phrases via a built-in regex list.
140
+
141
+ **Default patterns include:**
142
+ - `ignore all previous instructions`
143
+ - `disregard prior rules`
144
+ - `forget above prompts`
145
+ - `system prompt:`
146
+ - `you are now a/an ...`
147
+ - `act as a/an ...`
148
+ - `pretend you are ...`
149
+ - `jailbreak`, `DAN mode`, `developer mode`
150
+
151
+ ```ruby
152
+ # Default patterns only
153
+ Phronomy::Guardrail::Builtin::PromptInjectionDetector.new
154
+
155
+ # With additional custom patterns
156
+ Phronomy::Guardrail::Builtin::PromptInjectionDetector.new(
157
+ additional_patterns: [/do anything now/i, /DAN/]
158
+ )
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 7. Error Handling
164
+
165
+ When a guardrail fails, `Phronomy::GuardrailError` is raised. Callers should
166
+ rescue this exception to provide user-facing error messages:
167
+
168
+ ```ruby
169
+ begin
170
+ result = agent.invoke(user_input)
171
+ rescue Phronomy::GuardrailError => e
172
+ puts "Request blocked: #{e.message}"
173
+ end
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 8. Design Decisions
179
+
180
+ | Decision | Rationale |
181
+ |----------|-----------|
182
+ | Separate Input/Output subclasses | Makes type intent explicit; avoids guards accidentally applied to wrong phase |
183
+ | `run!` calls `check` | Decouples the check logic from error-raising boilerplate |
184
+ | Built-in PII detector uses regex only | Zero LLM cost; sufficient for structured PII like credit cards and My Number |
185
+ | `fail!` raises immediately | Short-circuits remaining guardrails; fail-fast semantics are safer |
186
+ | Multiple guardrails per agent | Composable; each guardrail has a single responsibility |