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.
- checksums.yaml +4 -4
- data/.mutant.yml +3 -4
- data/CHANGELOG.md +200 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +152 -0
- data/docs/architecture/persistence.md +175 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +82 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/README.md +172 -0
- data/docs/features.md +31 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/persistence-backends.md +88 -38
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +19 -15
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +145 -72
- data/lib/phronomy/agent/base.rb +388 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
- data/lib/phronomy/agent/recovery_support.rb +512 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +336 -102
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime.rb +77 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
- data/lib/phronomy/multi_agent/coordinator.rb +154 -0
- data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/handoff.rb +24 -45
- data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
- data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
- data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
- data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
- data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
- data/lib/phronomy/multi_agent/runner.rb +98 -0
- data/lib/phronomy/persistence/durable_codec.rb +646 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +210 -134
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +316 -0
- data/lib/phronomy/persistence.rb +81 -41
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/multi_agent.rbs +39 -0
- data/sig/phronomy/persistence.rbs +62 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +65 -17
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- 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 `
|
|
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
|
-
|
|
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
|
-
|
|
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.
|