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,175 @@
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 — Rails Integration
9
+
10
+ > **Archived.** Rails integration (`Railtie`, `acts_as_phronomy_message`,
11
+ > install generator, `AgentJob`) was removed in v0.3.0–v0.5.1. This document
12
+ > no longer describes the current public API and is preserved for historical
13
+ > reference only.
14
+
15
+ ## 1. Overview
16
+
17
+ `phronomy-rails` provides first-class Rails integration:
18
+
19
+ - `AgentJob` — run agents asynchronously with real-time streaming via ActionCable
20
+ - `Railtie` — auto-require phronomy components when Rails+ActiveRecord loads
21
+ - `acts_as_phronomy_message` — ActiveRecord mixin for storing LLM messages
22
+ - Generator — scaffolds database tables and initialiser
23
+
24
+ ---
25
+
26
+ ## 2. Railtie
27
+
28
+ `lib/phronomy/railtie.rb`
29
+
30
+ Registered automatically when `phronomy` is added to the Gemfile in a Rails
31
+ app. Hooks into `ActiveSupport.on_load(:active_record)` to:
32
+
33
+ 1. Require `phronomy/state_store/active_record`
34
+ 2. Require `phronomy/rails/acts_as_phronomy_message`
35
+
36
+ No manual `require` is needed in `config/application.rb`.
37
+
38
+ ---
39
+
40
+ ## 3. AgentJob
41
+
42
+ `lib/phronomy/rails/agent_job.rb`
43
+
44
+ An `ActiveJob::Base` subclass that runs a phronomy agent and optionally streams
45
+ events to an ActionCable channel.
46
+
47
+ ### Class method
48
+
49
+ ```ruby
50
+ MyAgentJob.perform_later(
51
+ agent_class: "PolicyAgent", # String — constantized at runtime
52
+ input: "What is policy?",
53
+ channel: "ConversationChannel", # ActionCable channel class name
54
+ stream: "conversation_42", # ActionCable stream name
55
+ config: { thread_id: "t1" } # forwarded to agent.invoke
56
+ )
57
+ ```
58
+
59
+ ### Event protocol
60
+
61
+ Events are broadcast to `stream` via `ActionCable.server.broadcast`:
62
+
63
+ ```json
64
+ { "type": "token", "content": "..." }
65
+ { "type": "done", "output": "full answer" }
66
+ { "type": "error", "message": "error description" }
67
+ ```
68
+
69
+ Token events are emitted during streaming (if the agent supports `stream:`).
70
+ The `done` event is emitted after the full output is available.
71
+ The `error` event is emitted if the agent raises an exception.
72
+
73
+ ### Perform method
74
+
75
+ ```ruby
76
+ def perform(agent_class:, input:, channel: nil, stream: nil, config: {})
77
+ agent = agent_class.constantize.new
78
+ output = agent.invoke(input, config: config)
79
+ broadcast(channel, stream, type: "done", output: output[:output]) if channel
80
+ rescue => e
81
+ broadcast(channel, stream, type: "error", message: e.message) if channel
82
+ raise
83
+ end
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 4. acts_as_phronomy_message
89
+
90
+ `lib/phronomy/rails/acts_as_phronomy_message.rb`
91
+
92
+ ActiveRecord mixin. Adds a `role` enum, `content` accessor, and helper scopes.
93
+
94
+ ```ruby
95
+ class Message < ApplicationRecord
96
+ acts_as_phronomy_message
97
+ end
98
+ ```
99
+
100
+ **Expected columns:**
101
+
102
+ | Column | Type | Description |
103
+ |--------|------|-------------|
104
+ | `role` | `integer` | `:system`, `:user`, `:assistant`, `:tool` |
105
+ | `content` | `text` | Message content |
106
+ | `thread_id` | `string` | Groups messages by conversation |
107
+ | `tool_call_id` | `string` (optional) | For tool result messages |
108
+
109
+ **Scopes added:**
110
+
111
+ | Scope | Description |
112
+ |-------|-------------|
113
+ | `for_thread(id)` | Filter by `thread_id` |
114
+ | `conversation_order` | Order by `created_at ASC` |
115
+ | `as_phronomy_messages` | Returns an Array of `{role:, content:}` hashes |
116
+
117
+ ---
118
+
119
+ ## 5. Database Schema
120
+
121
+ Generated by `rails generate phronomy:install`:
122
+
123
+ ### `phronomy_states`
124
+
125
+ ```sql
126
+ CREATE TABLE phronomy_states (
127
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
128
+ thread_id VARCHAR(255) NOT NULL,
129
+ state_json TEXT NOT NULL,
130
+ created_at DATETIME,
131
+ updated_at DATETIME,
132
+ UNIQUE INDEX idx_phronomy_states_thread_id (thread_id)
133
+ );
134
+ ```
135
+
136
+ ### `phronomy_messages`
137
+
138
+ ```sql
139
+ CREATE TABLE phronomy_messages (
140
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
141
+ thread_id VARCHAR(255) NOT NULL,
142
+ role INTEGER NOT NULL,
143
+ content TEXT NOT NULL,
144
+ tool_call_id VARCHAR(255),
145
+ created_at DATETIME,
146
+ updated_at DATETIME,
147
+ INDEX idx_phronomy_messages_thread_id (thread_id)
148
+ );
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 6. Generator
154
+
155
+ `lib/generators/phronomy/install_generator.rb`
156
+
157
+ ```bash
158
+ rails generate phronomy:install
159
+ ```
160
+
161
+ Creates:
162
+ - `db/migrate/<timestamp>_create_phronomy_tables.rb` (both tables above)
163
+ - `config/initializers/phronomy.rb` (skeleton Phronomy.configure block)
164
+
165
+ ---
166
+
167
+ ## 7. Design Decisions
168
+
169
+ | Decision | Rationale |
170
+ |----------|-----------|
171
+ | `AgentJob` uses string class name for `agent_class` | ActiveJob serialises arguments to JSON; class references must be strings |
172
+ | Railtie hooks into `on_load(:active_record)` | Avoids eager-loading AR models before the app is ready |
173
+ | `acts_as_phronomy_message` is a mixin, not a base class | Apps may extend their own Message model; mixin is less invasive |
174
+ | Generator creates both tables | Convenience; apps can remove the table they don't need |
175
+ | Broadcast `type:` field | Allows clients to handle events polymorphically without type sniffing |
@@ -0,0 +1,122 @@
1
+ > **HISTORICAL / non-normative snapshot**
2
+ >
3
+ > Preserved as design history. This document does not define current Phronomy
4
+ > architecture or public compatibility. Start at
5
+ > [docs/architecture.md](../../../architecture.md) and the
6
+ > [ADR index](../../../decisions/README.md).
7
+
8
+ # Ruby AI Agent Framework — Design Philosophy and Goals
9
+
10
+ ## 1. Overview
11
+
12
+ This framework (working name: **Phronomy**) is an AI agent construction framework for the Ruby ecosystem that provides functionality equivalent to LangChain and LangGraph. It leverages the `ruby_llm` gem for LLM communication and aims to deliver chain composition, state-graph execution, memory management, and multi-agent coordination through a Ruby-idiomatic DSL.
13
+
14
+ ---
15
+
16
+ ## 2. Problems We Are Solving
17
+
18
+ Currently, the Ruby/Rails ecosystem lacks a framework that provides the following features in an integrated way.
19
+
20
+ | Feature | Python Options | Ruby Status |
21
+ |---|---|---|
22
+ | LLM Abstraction | LangChain Core, LiteLLM | RubyLLM (available) |
23
+ | Chain Composition | LangChain LCEL | **implemented** (`Chain`) |
24
+ | State Graph Execution | LangGraph StateGraph | **implemented** (`Workflow` DSL + `WorkflowRunner`) |
25
+ | Checkpoint Persistence | LangGraph Checkpoint | **implemented** (`StateStore`) |
26
+ | Multi-agent Coordination | CrewAI / LangGraph | **implemented** (`Agent`, handoff) |
27
+ | Conversation Memory | LangChain Memory, mem0 | **implemented** (`Memory`) |
28
+ | Human-in-the-Loop | LangGraph interrupt | **implemented** (`wait_state` + `send_event`) |
29
+ | Observability / Tracing | LangSmith, Langfuse | **implemented** (`Tracer`, Langfuse) |
30
+
31
+ ---
32
+
33
+ ## 3. Design Principles
34
+
35
+ ### 3.1 Ruby-Idiom First
36
+
37
+ Rather than a direct translation of Python frameworks, this framework is redesigned to align with Ruby's idiomatic expressions, DSL culture, and type system. Instead of Pythonic `@decorator` patterns, it uses Ruby's `include`, `define_method`, blocks/Procs, and module mixins to provide equivalent functionality.
38
+
39
+ ### 3.2 Progressive Adoption
40
+
41
+ - **Level 0**: Simple chat and tool calls where RubyLLM alone is sufficient
42
+ - **Level 1**: Compose prompt → LLM → parser pipelines with Phronomy's Chain
43
+ - **Level 2**: Define multi-step workflows and agent loops with Workflow
44
+ - **Level 3**: Add state persistence and suspend/resume with Memory/StateStore
45
+ - **Level 4**: Coordinate multiple agents with Multi-agent
46
+
47
+ Each level can be adopted independently, so there is no need to introduce the entire framework at once.
48
+
49
+ ### 3.3 Rails Integration as First Class
50
+
51
+ Like RubyLLM, Phronomy ships with a `railtie` and provides `acts_as_*` DSL for ActiveRecord model integration. The cost of embedding it into a Rails app is minimized.
52
+
53
+ ### 3.4 Replaceable LLM Abstraction Layer
54
+
55
+ RubyLLM serves as the standard backend, but an interface is defined so that other LLM clients can be plugged in as extension points.
56
+
57
+ ### 3.5 Minimal Dependencies
58
+
59
+ The core depends only on RubyLLM and the Ruby standard library. Optional features (Redis checkpointer, vector search, Observer integration) are separated as optional gems.
60
+
61
+ ---
62
+
63
+ ## 4. Scope
64
+
65
+ ### 4.1 What This Framework Provides
66
+
67
+ - **Chain**: A composable pipeline of prompt template → LLM → output parser
68
+ - **Agent**: A reusable execution unit with tools, instructions, and LLM settings
69
+ - **Workflow**: A state-based agent workflow defined via a statechart DSL (`Phronomy::Workflow`)
70
+ - **Memory**: Conversation history management (short-term and long-term), context window management
71
+ - **StateStore**: Persistence, suspension, and resumption of workflow execution state
72
+ - **Tool**: Function definitions callable from LLM (extends RubyLLM's Tool)
73
+ - **Guardrail**: Input/output validation and constraints
74
+ - **Tracer**: Execution trace collection and output
75
+
76
+ ### 4.2 What This Framework Does Not Provide (Out of Scope)
77
+
78
+ - LLM provider API implementations (delegated to RubyLLM)
79
+ - Vector DB implementations (provided externally via adapters)
80
+ - Frontend UI / Chat UI
81
+ - RAG index construction pipelines (usable as tools)
82
+ - Model fine-tuning
83
+
84
+ ---
85
+
86
+ ## 5. Referenced OSS and Their Role
87
+
88
+ | OSS | Concepts Referenced | Corresponding Component |
89
+ |---|---|---|
90
+ | LangChain Core | Runnable / LCEL chain composition | Chain component |
91
+ | LangGraph | StateGraph, Pregel execution, Checkpoint | `Workflow` DSL / `WorkflowRunner` / `StateStore` components |
92
+ | CrewAI | Agent role-separation model (Crew/Task dropped; Agent-as-Tool adopted instead) | Agent component |
93
+ | OpenAI Agents SDK | Handoff, Guardrail, Tracing | Handoff / Guardrail / Tracer |
94
+ | RubyLLM | LLM abstraction, Tool, Rails integration | Used as the LLM abstraction layer |
95
+ | mem0 | Multi-level memory design | Reference for Memory component design |
96
+
97
+ ---
98
+
99
+ ## 6. Target Users
100
+
101
+ | User Group | Use Case |
102
+ |---|---|
103
+ | Rails developers | Add AI agent features to existing Rails applications |
104
+ | Ruby backend engineers | Automate AI workflows in batch processing or API services |
105
+ | AI application developers | Build LLM applications in Ruby as a Python alternative |
106
+
107
+ ---
108
+
109
+ ## 7. Name Candidates
110
+
111
+ | Candidate | Origin / Intent |
112
+ |---|---|
113
+ | **Phronomy** | Agent + workflow/graph flow. Intuitive |
114
+ | **Raix** | Ruby AI eXecution. Short and memorable |
115
+ | **Flowable** | Emphasizes flow/graph composition. Ruby-style `-able` |
116
+ | **Kenna** | Ruby-inspired name |
117
+
118
+ This document uses **Phronomy** as the working name.
119
+
120
+ ---
121
+
122
+ *Reference documents: AI Agent Design Guide Part I (00-preface, 01-overview, 02-scope)*
@@ -0,0 +1,178 @@
1
+ > **HISTORICAL / non-normative snapshot**
2
+ >
3
+ > Preserved as design history. This document does not define current Phronomy
4
+ > architecture or public compatibility. Start at
5
+ > [docs/architecture.md](../../../architecture.md) and the
6
+ > [ADR index](../../../decisions/README.md).
7
+
8
+ # RubyLLM Evaluation Report — Adoption Assessment as Phronomy Backend
9
+
10
+ ## 1. Overview
11
+
12
+ This document evaluates the pros and cons of adopting the `ruby_llm` gem as the LLM abstraction layer for Phronomy. LangChain Core (Python) and LiteLLM (Python) are referenced as comparison targets.
13
+
14
+ ---
15
+
16
+ ## 2. Current Capabilities of RubyLLM
17
+
18
+ Features provided by RubyLLM as of the current state (May 2026).
19
+
20
+ | Feature | Status | Notes |
21
+ |---|---|---|
22
+ | Multi-provider support | ✅ | OpenAI, Anthropic, Gemini, Bedrock, Azure, Ollama, Mistral, DeepSeek, etc. 13+ |
23
+ | Chat API | ✅ | `RubyLLM.chat` |
24
+ | Tool calling (Function Calling) | ✅ | `RubyLLM::Tool` DSL |
25
+ | Agent DSL | ✅ | `RubyLLM::Agent` class-based |
26
+ | Streaming | ✅ | SSE-based |
27
+ | Embeddings | ✅ | `RubyLLM.embed` |
28
+ | Image generation | ✅ | `RubyLLM.paint` |
29
+ | Audio transcription | ✅ | `RubyLLM.transcribe` |
30
+ | Moderation | ✅ | `RubyLLM.moderate` |
31
+ | Structured output | ✅ | Via `ruby_llm-schema` gem |
32
+ | Extended Thinking | ✅ | `thinking.rb` |
33
+ | Rails / ActiveRecord integration | ✅ | `acts_as_chat`, `acts_as_message` |
34
+ | Rails generators | ✅ | install, agent, tool, chat UI, etc. |
35
+ | Model registry | ✅ | 800+ models, capability/pricing information |
36
+ | Async support | ✅ | Asynchronous execution |
37
+ | Multimodal | ✅ | Image, audio, PDF attachments |
38
+
39
+ RubyLLM covers nearly all the functionality needed as an "LLM abstraction layer" and has rich Rails integration.
40
+
41
+ ---
42
+
43
+ ## 3. Evaluation from the Phronomy Perspective
44
+
45
+ ### 3.1 Advantages
46
+
47
+ #### (A) High completeness as an LLM abstraction layer
48
+
49
+ Provider differences (API specs, authentication, streaming, tool calling formats) are already handled, so Phronomy development does not need to build the LLM communication layer from scratch. Support for 13+ providers is extremely important in production.
50
+
51
+ #### (B) Tool definitions are easy to use as a Ruby DSL
52
+
53
+ ```ruby
54
+ class WebSearch < RubyLLM::Tool
55
+ description "Searches the web for information"
56
+
57
+ param :query, type: :string, desc: "The search query"
58
+ param :max_results, type: :integer, desc: "Maximum results", required: false
59
+
60
+ def execute(query:, max_results: 5)
61
+ # implementation
62
+ end
63
+ end
64
+ ```
65
+
66
+ Phronomy's Tool component can be designed to inherit and extend this DSL.
67
+
68
+ #### (C) Affinity with Rails / ActiveRecord
69
+
70
+ `acts_as_chat` and `acts_as_message` allow DB persistence of conversation history directly. Useful as the foundation for Phronomy's `StateStore::ActiveRecord` implementation.
71
+
72
+ #### (D) Built-in model registry
73
+
74
+ Model capabilities (whether function calling is supported, vision support, etc.) can be retrieved from the registry. Useful when Phronomy makes "dynamic decisions based on the capabilities of the model in use."
75
+
76
+ #### (E) Minimal dependencies with low security risk
77
+
78
+ Main dependencies are only `faraday` (HTTP) and `event_stream_parser` (SSE). Compared to frameworks with many dependencies, the security supply-chain risk is small.
79
+
80
+ #### (F) Commitment to the Ruby ecosystem
81
+
82
+ The combination of Rails generators, `railtie.rb`, and `acts_as_*` DSL shows strong commitment to the Ruby/Rails community. Phronomy can inherit the same culture.
83
+
84
+ #### (G) Streaming support is built-in
85
+
86
+ SSE-based streaming is implemented as standard, so Phronomy's streaming UX features (progressive token display) can be used at no additional cost.
87
+
88
+ ---
89
+
90
+ ### 3.2 Disadvantages and Limitations
91
+
92
+ #### (A) Agent orchestration features are too basic
93
+
94
+ `RubyLLM::Agent` can declare model/instructions/tools/temperature to run a single agent, but does not support:
95
+
96
+ - **State graph**: Workflow execution defining multiple steps with states and transitions
97
+ - **Checkpoint**: Suspend/resume of graph state (ActiveRecord persistence exists, but not AgentLoop state saving)
98
+ - **Multi-agent coordination**: Agent-to-agent handoff / delegation
99
+ - **Human-in-the-Loop**: Mechanism to pause execution and wait for user approval
100
+
101
+ → **Mitigation**: Phronomy implements these as an upper layer. RubyLLM's Agent is positioned internally as an "execution node for LLM calls."
102
+
103
+ #### (B) No context management or conversation compression
104
+
105
+ Chat accumulates conversation history, but there is no context compression, summarization, or deletion policy against token limits. Context window overflow occurs in long agent loops.
106
+
107
+ → **Mitigation**: Phronomy's `Memory` / `ContextManager` components provide conversation compression, summaries, and sliding windows.
108
+
109
+ #### (C) No chain composition (pipeline) mechanism
110
+
111
+ There is no mechanism for chain composition via `|` operator like LCEL (LangChain Expression Language), or for defining pipelines of prompt template → LLM → output parser.
112
+
113
+ → **Mitigation**: Phronomy's `Chain` component provides pipeline composition via `>>` operator (or `|`).
114
+
115
+ #### (D) Limited output parsers
116
+
117
+ Structured output via `ruby_llm-schema` exists, but there is no flexible output formatting/transformation layer equivalent to LangChain's diverse `OutputParser` (JSON, XML, Markdown, Pydantic, etc.).
118
+
119
+ → **Mitigation**: Phronomy's `OutputParser` component provides various parsers.
120
+
121
+ #### (E) No observability or tracing
122
+
123
+ No execution trace collection or output to LangSmith or Langfuse. Lacks the debugging/monitoring infrastructure needed in production.
124
+
125
+ → **Mitigation**: Phronomy's `Tracer` component provides OpenTelemetry-based tracing.
126
+
127
+ #### (F) Community and ecosystem are small compared to Python
128
+
129
+ No massive plugin ecosystem or integration library collection like LangChain or CrewAI. Third-party Tool/Integration beyond provider integration is limited.
130
+
131
+ → **Mitigation**: Phronomy implements MCP (Model Context Protocol) support so that Python's MCP tool ecosystem can be used from Ruby.
132
+
133
+ #### (G) No Pregel/Workflow runtime
134
+
135
+ There is no Ruby implementation equivalent to LangGraph's core Pregel computation model (workflow state scheduling, parallel execution, channel-based value propagation).
136
+
137
+ → **Mitigation**: Phronomy implements a lightweight Pregel-like graph runtime (simple version initially, extended later).
138
+
139
+ #### (H) Integration with Ruby's async processing ecosystem
140
+
141
+ Ruby's async processing (Async gem, Fiber-based) is still developing compared to Python's asyncio. Async execution design in Phronomy requires careful judgment.
142
+
143
+ → **Mitigation**: Support synchronous execution fully first, then provide async incrementally with Fiber/Thread.
144
+
145
+ ---
146
+
147
+ ## 4. Adoption Decision
148
+
149
+ ### 4.1 Reasons for Adopting (Recommended)
150
+
151
+ RubyLLM covers all necessary functionality as an "LLM abstraction layer" and has made significant investment in the Ruby/Rails ecosystem. By implementing the missing "framework layer" (graph execution, chain composition, memory management) as an upper layer, a collaborative design that leverages each other's strengths is possible.
152
+
153
+ ### 4.2 Alternatives if Not Adopted
154
+
155
+ | Alternative | Issues |
156
+ |---|---|
157
+ | Implement from scratch with direct Faraday | Enormous implementation cost for provider support |
158
+ | Langchainrb gem | Maintenance status unstable, limited features |
159
+ | Call Python LangChain via Ruby FFI | Loses Ruby idiom, deployment complexity |
160
+ | OpenAI Ruby gem alone | Locked to OpenAI, no multi-provider |
161
+
162
+ ### 4.3 Adoption Form
163
+
164
+ - **Core dependency**: Require `ruby_llm` as a mandatory dependency
165
+ - **Extension point**: Define a `Phronomy::LLM::Base` interface so adapters for non-RubyLLM clients can be connected in the future
166
+ - **Tool inheritance**: `Phronomy::Tool` inherits (or delegates to) `RubyLLM::Tool` to provide additional functionality
167
+ - **Agent usage**: Use `RubyLLM::Agent` internally while wrapping it as a Phronomy Workflow node
168
+
169
+ ---
170
+
171
+ ## 5. Version Requirements
172
+
173
+ ```ruby
174
+ # Gemfile
175
+ gem 'ruby_llm', '>= 1.3' # After Tool DSL, Agent, Streaming, Rails integration stabilized
176
+ ```
177
+
178
+ ---
@@ -0,0 +1,143 @@
1
+ > **HISTORICAL / non-normative snapshot**
2
+ >
3
+ > Preserved as design history. This document does not define current Phronomy
4
+ > architecture or public compatibility. Start at
5
+ > [docs/architecture.md](../../../architecture.md) and the
6
+ > [ADR index](../../../decisions/README.md).
7
+
8
+ # Phronomy — Design Decision Log
9
+
10
+ A record of design decisions and their rationale. Use this as a reference when revisiting decisions later.
11
+
12
+ ---
13
+
14
+ ## Decision 1: Adopt RubyLLM as the LLM abstraction layer
15
+
16
+ **Situation**: Needed to choose between a custom implementation and RubyLLM as the backend for Phronomy.
17
+
18
+ **Choice**: Adopt RubyLLM.
19
+
20
+ **Rationale**:
21
+ - Provider support is already implemented, reducing Phronomy's development cost.
22
+ - Rails/ActiveRecord integration, streaming, and model registry cover foundations Phronomy needs.
23
+ - Natural Ruby DSL code aligns well with Phronomy's DSL.
24
+ - Focusing on Agent/Workflow semantics provides better return than rebuilding provider clients.
25
+
26
+ **Trade-offs**:
27
+ - RubyLLM API changes must be tracked.
28
+ - Providers outside RubyLLM may lag.
29
+ - `Phronomy::LLMAdapter::Base` keeps the provider boundary replaceable.
30
+
31
+ ---
32
+
33
+ ## Decision 2: Manage state as immutable objects
34
+
35
+ **Situation**: Needed to choose between mutable hashes and immutable objects for Workflow state management.
36
+
37
+ **Choice**: `State#merge` immutably returns a new object.
38
+
39
+ **Rationale**:
40
+ - Checkpoint saving and comparison are easier.
41
+ - Accidental cross-node mutation is reduced.
42
+ - History tracking and debugging are clearer.
43
+ - Parallel logical execution has fewer shared-mutation hazards.
44
+
45
+ **Trade-offs**:
46
+ - State updates copy objects.
47
+ - Initial implementation favors shallow immutable transitions over deep-copy complexity.
48
+
49
+ ---
50
+
51
+ ## Decision 3: Use `>>` operator for Chain composition (`|` as alias)
52
+
53
+ **Situation**: Needed a Ruby composition syntax inspired by LCEL.
54
+
55
+ **Choice**: `>>` is primary and `|` remains an alias.
56
+
57
+ **Rationale**:
58
+ - `>>` is consistent with Ruby `Proc#>>` function composition.
59
+ - `|` remains familiar to LangChain users.
60
+
61
+ ---
62
+
63
+ ## Decision 4: Implement the Workflow graph runtime in-house
64
+
65
+ **Situation**: General graph gems do not model stateful Agent execution, checkpoints, suspension, or events well.
66
+
67
+ **Choice**: Maintain the specialized Workflow/FSM execution engine in Phronomy.
68
+
69
+ **Rationale**:
70
+ - Agent lifecycle semantics are more important than general graph algorithms.
71
+ - Explicit state/event behavior integrates directly with persistence and suspension.
72
+
73
+ **Trade-offs**:
74
+ - Runtime correctness remains a framework responsibility.
75
+
76
+ ---
77
+
78
+ ## Decision 5: Unify memory scope by thread
79
+
80
+ **Situation**: Needed a simple durable context scope before adding multi-level memory.
81
+
82
+ **Choice**: Use `thread_id` as the primary conversation/session scope.
83
+
84
+ **Rationale**:
85
+ - It covers the common conversation lifecycle.
86
+ - Applications may map domain identities such as user IDs onto thread IDs.
87
+
88
+ ---
89
+
90
+ ## Decision 6: Security — Tool execution is no-sandbox by default
91
+
92
+ **Situation**: Process sandboxing would add deployment-specific dependencies to the core framework.
93
+
94
+ **Choice**: No sandbox by default. Use approval policy, guardrails, and application-owned deployment isolation.
95
+
96
+ **Risk mitigation**:
97
+ - Irreversible operations should require approval.
98
+ - Shell/process capabilities are application-defined rather than implicitly enabled.
99
+
100
+ ---
101
+
102
+ ## Decision 7: EventLoop/FSMSession owns lifecycle; OffloadPool owns synchronous offload
103
+
104
+ **Situation**: Phronomy needs many concurrently waiting Agent, Workflow, Tool,
105
+ and MultiAgent lifecycles without one OS Thread per logical task. It also needs a
106
+ bounded place for synchronous work that must not block EventLoop.
107
+
108
+ **Choice**: Framework lifecycle coordination uses one Runtime-owned EventLoop and
109
+ explicit FSMSession state/events. `Phronomy::Task` is only a completion handle.
110
+ Synchronous work that cannot safely run on EventLoop uses the bounded
111
+ `OffloadPool`.
112
+
113
+ Tool execution classification is deliberately limited to:
114
+
115
+ ```text
116
+ :cooperative
117
+ :offloaded
118
+ ```
119
+
120
+ The application decides whether work is safe on EventLoop. The framework does
121
+ not separately classify I/O-bound, CPU-bound, or process work. Blocking I/O,
122
+ CPU-bound synchronous Ruby work, and other long synchronous calls may all use
123
+ `:offloaded`.
124
+
125
+ **Rationale**:
126
+ - Waiting for another Agent, approval, timer, or event does not require an OS Thread.
127
+ - Agent/Workflow/Tool/MultiAgent share one explicit continuation model.
128
+ - The distinction Phronomy must enforce is EventLoop-safe versus off-EventLoop, not I/O versus CPU.
129
+ - A bounded shared OffloadPool provides backpressure without inventing a separate executor taxonomy prematurely.
130
+ - Named pools allow applications to isolate workloads when shared capacity is insufficient.
131
+
132
+ **Trade-offs**:
133
+ - FSM actions must return promptly and explicitly model later completion events.
134
+ - Applications own `offload_pool_size`, queue sizing, and workload-mix capacity planning.
135
+ - Thread offload does not provide CPU isolation, remove CRuby GVL contention, or guarantee I/O/CPU fairness.
136
+ - A future subprocess offload facility may add stronger CPU/process isolation without changing the Tool execution-mode contract.
137
+ - Synchronous public wrappers must not be called from the EventLoop thread.
138
+
139
+ See ADR-010 for the authoritative current concurrency model.
140
+
141
+ ---
142
+
143
+ *References: 00_design_philosophy.md, 01_rubyllm_evaluation.md, AI Agent Design Guide 02-scope.md*
@@ -815,7 +815,7 @@ current at the time of each release.
815
815
  ### Maintenance
816
816
 
817
817
  - **Archive stale Rails integration design doc** (#91): Added an archived
818
- notice to `spec/design/17_rails_integration.md` clarifying that Rails
818
+ notice to `docs/archive/design/archived/17_rails_integration.md` clarifying that Rails
819
819
  integration was removed in v0.3.0–v0.5.1 and the document is for
820
820
  historical reference only.
821
821
 
@@ -2,7 +2,12 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Accepted
5
+ Superseded
6
+
7
+ Superseded by
8
+ [`027-llm-adapter-provider-boundary`](027-llm-adapter-provider-boundary.md).
9
+ The historical rationale for adopting RubyLLM is preserved below; the direct
10
+ Provider-call boundary described by this ADR is no longer normative.
6
11
 
7
12
  ## Context
8
13
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Accepted
5
+ Amended
6
6
 
7
7
  ## Context
8
8
 
@@ -40,3 +40,28 @@ object. The framework replaces the current state with the returned value.
40
40
  - Node authors must remember to return the new state; forgetting to do so
41
41
  discards the update silently. (Future work: frozen state objects or
42
42
  a strict return type could catch this at dev time.)
43
+
44
+ ## Amendment — EventLoop-owned direct field mutation
45
+
46
+ The original Decision remains normative for `WorkflowContext#merge`, but its
47
+ statement that **all field writers** return a new `WorkflowContext` is amended.
48
+
49
+ Current semantics are:
50
+
51
+ 1. `WorkflowContext#merge` remains a non-mutating, new-instance operation. It
52
+ applies each field's merge policy and returns a replacement
53
+ `WorkflowContext`.
54
+ 2. Direct generated field writers are controlled mutation APIs. They update the
55
+ current `WorkflowContext` instance only when code is executing under
56
+ Phronomy's Runtime/EventLoop write authority.
57
+ 3. A direct field write outside EventLoop dispatch authority fails with
58
+ `Phronomy::WorkflowContextOwnershipError`.
59
+ 4. EventLoop write authority and Ruby object immutability are distinct
60
+ state-management concerns. The single-writer rule does not require every live
61
+ state transition to allocate a replacement Ruby object.
62
+ 5. This amendment does not introduce or redefine Workflow identity. Durable
63
+ Workflow identity remains `workflow_instance_id`.
64
+
65
+ The original rationale for value-style `merge` remains useful: application code
66
+ can construct replacement state without mutating the source context, while
67
+ Runtime-owned event processing may use guarded direct writers when appropriate.
@@ -2,7 +2,8 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Amended (see Amendment section below)
5
+ Superseded by
6
+ [ADR-019: Filter Contract and Security Boundaries](019-filter-contract-and-security-boundaries.md).
6
7
 
7
8
  ## Context
8
9