phronomy 0.20.0 → 0.22.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 -1
- data/CHANGELOG.md +41 -11
- data/CONTRIBUTING.md +44 -6
- data/README.md +26 -6
- data/docs/decisions/010-cooperative-first-concurrency.md +86 -69
- data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +184 -0
- data/docs/features.md +22 -13
- data/docs/getting-started.md +36 -18
- data/docs/runtime-and-concurrency.md +110 -147
- data/examples/workflows/agent_event_mapping.rb +27 -30
- data/lib/phronomy/agent/async_event_api.rb +14 -4
- data/lib/phronomy/agent/tool_executor.rb +7 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +142 -245
- data/lib/phronomy/engine/task.rb +50 -6
- data/lib/phronomy/invocation_context.rb +11 -52
- data/lib/phronomy/llm_adapter/base.rb +29 -32
- data/lib/phronomy/llm_adapter/ruby_llm.rb +13 -12
- data/lib/phronomy/llm_adapter.rb +10 -7
- data/lib/phronomy/output_parser/base.rb +5 -1
- data/lib/phronomy/tool/base.rb +15 -0
- data/lib/phronomy/tool.rb +11 -0
- data/lib/phronomy/vector_store/async_backend.rb +15 -38
- data/lib/phronomy/vector_store/base.rb +12 -13
- data/lib/phronomy/vector_store/embeddings/base.rb +11 -9
- data/lib/phronomy/version.rb +1 -1
- data/scripts/run_mutation.sh +2 -1
- data/sig/phronomy/agent.rbs +36 -0
- data/sig/phronomy/extensions.rbs +50 -0
- data/sig/phronomy/llm_adapter.rbs +11 -0
- data/sig/phronomy/persistence.rbs +70 -0
- data/sig/phronomy/runtime.rbs +43 -0
- data/sig/phronomy/tool.rbs +39 -0
- data/sig/phronomy/workflow.rbs +30 -0
- data/sig/phronomy.rbs +49 -1
- metadata +12 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# ADR-015: Public Tool Façade, Extension SPI, and RBS Contract Boundary
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Phronomy has accumulated two related API-maintenance problems:
|
|
10
|
+
|
|
11
|
+
1. Application-defined Tools are implemented by subclassing the internal-taxonomy
|
|
12
|
+
name `Phronomy::Agent::Context::Capability::Base`, even though the product
|
|
13
|
+
concept exposed to users is simply a Tool.
|
|
14
|
+
2. The gem ships almost no RBS signatures, so the distinction between
|
|
15
|
+
application APIs, extension SPIs, and private implementation objects is not
|
|
16
|
+
machine-readable.
|
|
17
|
+
|
|
18
|
+
At the same time, Phronomy already has compatibility mechanisms that must remain
|
|
19
|
+
canonical: runtime behavior, focused contract tests, YARD `@api`
|
|
20
|
+
classification, feature stability documentation, and API snapshots. RBS must not
|
|
21
|
+
become a second specification system that silently promotes internal behavior to
|
|
22
|
+
a compatibility promise.
|
|
23
|
+
|
|
24
|
+
The LLM call boundary also needs an explicit extension contract. Configuration
|
|
25
|
+
already allows replacing `llm_adapter`, and `LLMAdapter::Base` requires
|
|
26
|
+
implementations of `complete` and `stream`, but those methods were previously
|
|
27
|
+
classified as private. The current Agent pipeline still materializes a
|
|
28
|
+
RubyLLM-shaped chat/runtime object before invoking the adapter; formalizing this
|
|
29
|
+
SPI therefore must not be described as a provider-neutral replacement of the
|
|
30
|
+
whole materialization pipeline.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
### Public Tool authoring façade
|
|
35
|
+
|
|
36
|
+
The application-facing Tool authoring name is:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Phronomy::Tool::Base
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The existing implementation class remains:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Phronomy::Agent::Context::Capability::Base
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`Phronomy::Tool::Base` is an exact constant alias to that same class object. It
|
|
49
|
+
is not a subclass and the implementation source is not moved.
|
|
50
|
+
|
|
51
|
+
This preserves:
|
|
52
|
+
|
|
53
|
+
- class identity;
|
|
54
|
+
- Tool DSL class-instance state;
|
|
55
|
+
- built-in Tool inheritance;
|
|
56
|
+
- existing user code using the longer namespace;
|
|
57
|
+
- existing serialization/debugging behavior based on the implementation class
|
|
58
|
+
name.
|
|
59
|
+
|
|
60
|
+
Consequently, `Phronomy::Tool::Base.name` intentionally remains
|
|
61
|
+
`"Phronomy::Agent::Context::Capability::Base"`. If a future public contract
|
|
62
|
+
requires the runtime class name itself to become `Phronomy::Tool::Base`, that is
|
|
63
|
+
a separate migration decision.
|
|
64
|
+
|
|
65
|
+
`Phronomy::Tool` means the authoring API. `Phronomy::Tools` continues to mean
|
|
66
|
+
Phronomy-provided built-in Tool classes.
|
|
67
|
+
|
|
68
|
+
### Extension dependency direction
|
|
69
|
+
|
|
70
|
+
Phronomy owns the interfaces that external implementations depend on:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
Phronomy Core ──────→ Phronomy-owned public contract
|
|
74
|
+
External extension ─→ Phronomy-owned public contract
|
|
75
|
+
External extension ─→ extension-specific dependency
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
External adapters/backends must not need private Runtime execution machinery
|
|
79
|
+
such as EventLoop, FSMSession, AgentInvocation, ExecutionCoordinator, or private
|
|
80
|
+
OffloadPool operation state merely to implement their domain contract.
|
|
81
|
+
|
|
82
|
+
Synchronous work requiring an operating-system worker Thread is submitted by
|
|
83
|
+
Phronomy to OffloadPool. The caller-facing completion handle is
|
|
84
|
+
`Phronomy::Task`; OffloadPool-specific queue/worker/abandonment state remains
|
|
85
|
+
private execution machinery as defined by ADR-010.
|
|
86
|
+
|
|
87
|
+
### LLMAdapter SPI
|
|
88
|
+
|
|
89
|
+
`Phronomy::LLMAdapter::Base` is a Beta extension SPI.
|
|
90
|
+
|
|
91
|
+
The external implementer contract is centered on:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
def complete(chat, message, config: {})
|
|
95
|
+
def stream(chat, message, config: {}, &block)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The adapter or provider client owns provider transport timeout, retry, backoff,
|
|
99
|
+
and rate-limit behavior.
|
|
100
|
+
|
|
101
|
+
Phronomy owns the asynchronous bridge:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
complete / stream
|
|
105
|
+
↓
|
|
106
|
+
complete_async / stream_async
|
|
107
|
+
↓
|
|
108
|
+
OffloadPool
|
|
109
|
+
↓
|
|
110
|
+
Task
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
External adapter implementations do not implement or depend on OffloadPool,
|
|
114
|
+
EventLoop, FSMSession, or other Runtime internals.
|
|
115
|
+
|
|
116
|
+
This SPI is the call boundary around the currently materialized chat/runtime
|
|
117
|
+
object. It does **not** make the entire pipeline provider-neutral. In particular,
|
|
118
|
+
this ADR does not remove `RubyLLMMaterializer`, introduce provider-neutral
|
|
119
|
+
request/response objects, or claim that arbitrary LLM client libraries can
|
|
120
|
+
replace RubyLLM without additional architecture work.
|
|
121
|
+
|
|
122
|
+
### RBS ownership and scope
|
|
123
|
+
|
|
124
|
+
RBS is a typed representation of contracts already established by Phronomy. The
|
|
125
|
+
source of API meaning remains the combination of:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
runtime behavior
|
|
129
|
+
+ YARD @api classification
|
|
130
|
+
+ feature/API documentation
|
|
131
|
+
+ focused compatibility/contract tests
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
RBS describes the resulting type shape. It must not be used in the opposite
|
|
135
|
+
direction to justify making an internal method public.
|
|
136
|
+
|
|
137
|
+
Initial signatures cover:
|
|
138
|
+
|
|
139
|
+
- primary application APIs;
|
|
140
|
+
- explicit extension SPIs;
|
|
141
|
+
- stable/common completion and configuration types needed to connect them.
|
|
142
|
+
|
|
143
|
+
Private implementation classes are not exhaustively signed.
|
|
144
|
+
|
|
145
|
+
Dynamic/external boundaries may use `untyped` where the framework does not own a
|
|
146
|
+
stable type shape. Phronomy-owned contracts such as Task result flow,
|
|
147
|
+
Persistence SPI methods, IDs/revisions, and documented fixed method arguments
|
|
148
|
+
should be typed where practical.
|
|
149
|
+
|
|
150
|
+
### Third-party RBS ownership
|
|
151
|
+
|
|
152
|
+
Phronomy does not vendor placeholder signatures for RubyLLM or other third-party
|
|
153
|
+
gems merely to make its own signatures validate. If a third-party type is not
|
|
154
|
+
available or is too unstable, the Phronomy-owned boundary uses an appropriate
|
|
155
|
+
`untyped` type until an authoritative signature is available.
|
|
156
|
+
|
|
157
|
+
### Validation and source dependency checks
|
|
158
|
+
|
|
159
|
+
RBS validation runs in a dedicated CI job so RBS tooling does not change the
|
|
160
|
+
Ruby 3.2 runtime support contract.
|
|
161
|
+
|
|
162
|
+
RBS verifies type/contract dependency shape. It does not prove Ruby source
|
|
163
|
+
`require` or constant dependency direction. Existing lightweight architecture
|
|
164
|
+
regression specs remain the appropriate place for source/runtime dependency
|
|
165
|
+
rules when such checks are needed.
|
|
166
|
+
|
|
167
|
+
Steep/full implementation type-checking is not introduced by this ADR.
|
|
168
|
+
|
|
169
|
+
## Consequences
|
|
170
|
+
|
|
171
|
+
- Tool authoring becomes shorter without moving the implementation class.
|
|
172
|
+
- Existing Tool definitions remain source-compatible.
|
|
173
|
+
- The alias has a deliberately different user-facing name and runtime
|
|
174
|
+
`Class#name`; that distinction is documented rather than hidden in API
|
|
175
|
+
snapshot machinery.
|
|
176
|
+
- `LLMAdapter::Base#complete` and `#stream` become explicit Beta extension
|
|
177
|
+
contracts while async execution stays framework-owned.
|
|
178
|
+
- RBS can describe external extension boundaries without exposing private
|
|
179
|
+
Runtime machinery.
|
|
180
|
+
- Public API review gains one additional artifact (`sig/**/*.rbs`) but does not
|
|
181
|
+
gain a second semantic source of truth.
|
|
182
|
+
- A future provider-neutral LLM architecture, genuine native-async backend SPI,
|
|
183
|
+
or implementation-class rename requires a separate ADR rather than being
|
|
184
|
+
smuggled into signature work.
|
data/docs/features.md
CHANGED
|
@@ -19,7 +19,9 @@ for production deployments.
|
|
|
19
19
|
|---|---|
|
|
20
20
|
| **Workflow** — Stateful, branching workflows with `wait_state` and explicit events | Stable |
|
|
21
21
|
| **Agent** — Stateful ReAct-style agents with stable `agent_id`, persistence-backed execution state, canonical history, guardrails, and conversation context | Stable |
|
|
22
|
+
| **Tool authoring façade** — `Phronomy::Tool::Base` is the public authoring name for the existing Capability base class; the legacy namespace remains compatible | Beta |
|
|
22
23
|
| **Unified Persistence** — One durable backend abstraction for Agent state and Workflow `workflow_states`; live Agent/Workflow state remains owned by the active instance/session between durable commits; custom backends implement the documented Backend SPI and repository/transaction semantics | Beta |
|
|
24
|
+
| **LLMAdapter SPI** — `Phronomy::LLMAdapter::Base#complete` / `#stream` define the Beta call-adapter extension boundary; Phronomy owns async/offload wrapping | Beta |
|
|
23
25
|
| **Before-Large-Language-Model (LLM) Input Hook** — Three-tier per-call LLM input customization via `before_llm_input` and `LLMInputPatch` | Stable |
|
|
24
26
|
| **Context Management** — Journal + Context Policy + per-LLM-call Manifest with token-budget-aware selection and protocol-safe Tool Call / Tool message dependencies | Stable |
|
|
25
27
|
| **Filters** — Input/output transformation and blocking via `Filter::Base` | Beta |
|
|
@@ -35,7 +37,8 @@ for production deployments.
|
|
|
35
37
|
|---|---|
|
|
36
38
|
| **Knowledge** — Journal-backed persistent Agent context registered with `knowledge:` / `add_knowledge`, selected per LLM call by Context Policy | Beta |
|
|
37
39
|
| **`VectorStore#size`** — Document count for InMemory, RedisSearch, and Pgvector backends | Beta |
|
|
38
|
-
|
|
|
40
|
+
| **VectorStore async convenience** — `add_async` / `search_async` / `remove_async` / `clear_async` offload the synchronous Backend SPI through Phronomy and return `Task`; native async override is not part of the current Backend SPI | Beta |
|
|
41
|
+
| **Embedding async convenience** — `embed_async` offloads synchronous `embed` through Phronomy and returns `Task` | Beta |
|
|
39
42
|
| **Model Context Protocol (MCP) Tool** — `Phronomy::Tools::Mcp` integration through the official `mcp` gem | Beta |
|
|
40
43
|
| **Agent Tool** — `Phronomy::Tools::Agent.from_agent` exposes a child Agent as a Tool without occupying a worker while waiting | Beta |
|
|
41
44
|
| **Vector Search Tool** — `Phronomy::Tools::VectorSearch` wraps VectorStore and Embeddings adapters | Beta |
|
|
@@ -50,6 +53,7 @@ for production deployments.
|
|
|
50
53
|
| **Agent async events** — `invoke_async(..., on_event:)` and `stream_async(..., on_event:)`; streaming additionally emits `:token` | Beta |
|
|
51
54
|
| **`stream` / `stream_async`** — Event callbacks execute on EventLoop and must return quickly | Beta |
|
|
52
55
|
| **`stream_callback_error_policy`** — Terminal event callback error policy (`:report` / `:fail_task`) | Beta |
|
|
56
|
+
| **Task completion contract** — `Task` is the common caller-facing completion handle for EventLoop/FSMSession lifecycles and OffloadPool work | Beta |
|
|
53
57
|
| **`Task#map`** — Application-level Task result transformation and error propagation | Stable |
|
|
54
58
|
| **CancellationToken** — Cooperative cancellation with explicit `cancel!`, lazy monotonic deadlines, and callback registration | Experimental |
|
|
55
59
|
| **Tool `execution_mode`** — `:cooperative` for short EventLoop-safe work; `:offloaded` for synchronous work that must stay off EventLoop | Experimental |
|
|
@@ -80,18 +84,23 @@ compatibility guarantees.
|
|
|
80
84
|
|
|
81
85
|
The YARD `@api` classification is independent from Ruby language visibility in
|
|
82
86
|
both directions. `@api public` marks a compatibility contract, but the Ruby
|
|
83
|
-
visibility still follows the intended calling model
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
visibility still follows the intended calling model. `@api private` means
|
|
88
|
+
"internal/no compatibility promise" and does not require a Ruby `private`
|
|
89
|
+
declaration; some internal methods remain Ruby-public because Phronomy components
|
|
90
|
+
call them through explicit receivers.
|
|
91
|
+
|
|
92
|
+
`Task` is the caller-facing completion abstraction. Framework components own
|
|
93
|
+
settlement (`complete` / `fail` / `cancel!`); application code observes Tasks via
|
|
94
|
+
`wait_result`, `on_complete`, `map`, and state readers. Operation-wide cancellation
|
|
95
|
+
is requested through the `CancellationToken` accepted by the API that created the
|
|
96
|
+
Task.
|
|
97
|
+
|
|
98
|
+
Persistence Backend SPI methods, LLMAdapter methods, and other documented
|
|
99
|
+
extension contracts are deliberate exceptions to the ordinary
|
|
100
|
+
application-facing interpretation of `@api public`: they are compatibility
|
|
101
|
+
contracts for implementers. Extension implementations must not depend on Runtime
|
|
102
|
+
private execution objects such as EventLoop/FSMSession/OffloadPool operation
|
|
103
|
+
records.
|
|
95
104
|
|
|
96
105
|
`Phronomy::StateStore` is no longer a public backend abstraction. Workflow
|
|
97
106
|
durability is provided through `Phronomy::Persistence#workflow_states`; see the
|
data/docs/getting-started.md
CHANGED
|
@@ -49,8 +49,12 @@ Install only the backend gems required by your application:
|
|
|
49
49
|
|
|
50
50
|
## Define a Tool and Agent
|
|
51
51
|
|
|
52
|
+
Use `Phronomy::Tool::Base` as the application-facing authoring API. It is an
|
|
53
|
+
exact alias of the existing `Phronomy::Agent::Context::Capability::Base`, so
|
|
54
|
+
existing Tool definitions using the longer namespace remain compatible.
|
|
55
|
+
|
|
52
56
|
```ruby
|
|
53
|
-
class WebSearch < Phronomy::
|
|
57
|
+
class WebSearch < Phronomy::Tool::Base
|
|
54
58
|
description "Search the web"
|
|
55
59
|
param :query, type: :string, desc: "Search query"
|
|
56
60
|
|
|
@@ -138,6 +142,24 @@ task = agent.invoke_async("Hello")
|
|
|
138
142
|
result = task.wait_result
|
|
139
143
|
```
|
|
140
144
|
|
|
145
|
+
Both `invoke` and `invoke_async` can receive public Agent events through either
|
|
146
|
+
an `on_event:` listener or a block. A block is convenient when the listener is
|
|
147
|
+
local to the call:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
task = agent.invoke_async("Hello") do |event|
|
|
151
|
+
puts event.payload[:output] if event.type == :done
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use `on_event:` when the listener already exists as a callable. Do not provide
|
|
156
|
+
both `on_event:` and a block to the same invocation.
|
|
157
|
+
|
|
158
|
+
`Phronomy::Task` is the common caller-facing completion handle for asynchronous
|
|
159
|
+
Phronomy work. Logical lifecycle progress is driven by EventLoop/FSMSession;
|
|
160
|
+
synchronous work that must execute away from EventLoop is submitted to
|
|
161
|
+
OffloadPool. Both paths expose completion as a `Task`.
|
|
162
|
+
|
|
141
163
|
`Task#wait_result` is for an external caller. Do not block EventLoop waiting for
|
|
142
164
|
a Task that can only complete through that same EventLoop.
|
|
143
165
|
|
|
@@ -150,13 +172,12 @@ end
|
|
|
150
172
|
```
|
|
151
173
|
|
|
152
174
|
```ruby
|
|
153
|
-
task = agent.stream_async(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
175
|
+
task = agent.stream_async("Explain the design") do |event|
|
|
176
|
+
puts event.payload if event.type == :token
|
|
177
|
+
end
|
|
157
178
|
```
|
|
158
179
|
|
|
159
|
-
|
|
180
|
+
Agent event callbacks execute on EventLoop and therefore should return quickly.
|
|
160
181
|
|
|
161
182
|
## Human-in-the-loop approval
|
|
162
183
|
|
|
@@ -274,18 +295,15 @@ workflow = Phronomy::Workflow.define(AnswerContext) do
|
|
|
274
295
|
entry :asking, ->(ctx) {
|
|
275
296
|
thread_id = ctx.thread_id
|
|
276
297
|
|
|
277
|
-
my_agent.invoke_async(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
)
|
|
298
|
+
my_agent.invoke_async(ctx.question) do |event|
|
|
299
|
+
next unless event.type == :done
|
|
300
|
+
|
|
301
|
+
workflow.signal(
|
|
302
|
+
thread_id: thread_id,
|
|
303
|
+
event: :answer_ready,
|
|
304
|
+
payload: {answer: event.payload[:output]}
|
|
305
|
+
)
|
|
306
|
+
end
|
|
289
307
|
|
|
290
308
|
ctx
|
|
291
309
|
}
|