phronomy 0.17.0 → 0.19.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/CHANGELOG.md +122 -1134
- data/README.md +70 -1280
- data/benchmark/bench_regression.rb +25 -5
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
- data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
- data/docs/features.md +93 -0
- data/docs/getting-started.md +387 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/migrations/0.19.md +148 -0
- data/docs/runtime-and-concurrency.md +349 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +116 -55
- data/lib/phronomy/agent/context/capability/base.rb +13 -3
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +423 -252
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/shared_state.rb +2 -0
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +97 -47
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/configuration.rb +7 -30
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +170 -193
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +52 -158
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
- data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +12 -3
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +363 -99
- data/lib/phronomy.rb +10 -4
- data/scripts/api_snapshot.rb +4 -4
- metadata +25 -33
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -193
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
This guide contains the setup and introductory examples that were previously
|
|
4
|
+
embedded in the repository README. The README remains the project entry point;
|
|
5
|
+
this document carries the longer operational examples.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add Phronomy to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "phronomy"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Phronomy uses RubyLLM for Large Language Model (LLM) access. Configure provider credentials and the
|
|
22
|
+
transport retry/timeout policy on RubyLLM itself:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
RubyLLM.configure do |c|
|
|
26
|
+
c.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
27
|
+
# c.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
28
|
+
|
|
29
|
+
c.request_timeout = 120
|
|
30
|
+
c.max_retries = 3
|
|
31
|
+
c.retry_interval = 0.1
|
|
32
|
+
c.retry_backoff_factor = 2
|
|
33
|
+
c.retry_interval_randomness = 0.5
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Phronomy does not add a second LLM transport timeout/retry layer on top of the
|
|
38
|
+
configured adapter.
|
|
39
|
+
|
|
40
|
+
## Optional dependencies
|
|
41
|
+
|
|
42
|
+
Install only the backend gems required by your application:
|
|
43
|
+
|
|
44
|
+
| Gem | Required for |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `pgvector` | `Phronomy::VectorStore::Pgvector` |
|
|
47
|
+
| `redis` | `Phronomy::VectorStore::RedisSearch` |
|
|
48
|
+
| `opentelemetry-api` | `Phronomy::Tracing::OpenTelemetryTracer` |
|
|
49
|
+
|
|
50
|
+
## Define a Tool and Agent
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class WebSearch < Phronomy::Agent::Context::Capability::Base
|
|
54
|
+
description "Search the web"
|
|
55
|
+
param :query, type: :string, desc: "Search query"
|
|
56
|
+
|
|
57
|
+
def execute(query:)
|
|
58
|
+
"Mock search result for: #{query}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class ResearchAgent < Phronomy::Agent::Base
|
|
63
|
+
agent_definition id: "research-agent", version: 1
|
|
64
|
+
model "gpt-4o"
|
|
65
|
+
instructions "You are a research assistant. Use tools to answer questions."
|
|
66
|
+
tools(WebSearch => nil)
|
|
67
|
+
max_iterations 5
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
result = ResearchAgent.new.invoke("Research Ruby AI frameworks")
|
|
71
|
+
puts result[:output]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Every concrete stateful Agent definition declares a stable definition ID and
|
|
75
|
+
version. The definition identity is checked when persisted Agent state is loaded.
|
|
76
|
+
|
|
77
|
+
## Stateful Agent persistence
|
|
78
|
+
|
|
79
|
+
Phronomy Agents own their conversation history and persistent Knowledge. The
|
|
80
|
+
application does not need to pass the previous `messages` array back into every
|
|
81
|
+
invocation.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
persistence = Phronomy::Persistence::InMemory.new
|
|
85
|
+
|
|
86
|
+
agent = ResearchAgent.create(
|
|
87
|
+
agent_id: "research-session-42",
|
|
88
|
+
knowledge: ["Customer tier: enterprise"],
|
|
89
|
+
persistence: persistence
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
agent.invoke("My name is Alice.")
|
|
93
|
+
agent.add_knowledge("Customer locale: ja-JP")
|
|
94
|
+
result = agent.invoke("What is my name?")
|
|
95
|
+
|
|
96
|
+
puts result[:output]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Load the same Agent again when the same Persistence backend is available:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
agent = ResearchAgent.load(
|
|
103
|
+
"research-session-42",
|
|
104
|
+
persistence: persistence
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
agent.invoke("Continue our previous discussion.")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`load` is the hydration boundary. While that Agent instance is live, the
|
|
111
|
+
instance and its active `AgentExecutionActivation` own the current logical state.
|
|
112
|
+
Phronomy persists snapshots at defined durability boundaries but does not reload
|
|
113
|
+
mutable Agent/Execution/Journal state before every LLM or Tool step. A conflicting
|
|
114
|
+
external durable write is surfaced as `Persistence::ConflictError` rather than
|
|
115
|
+
silently merged into the live instance.
|
|
116
|
+
|
|
117
|
+
The active transcript and Knowledge views can be advanced independently without
|
|
118
|
+
deleting the append-only canonical Journal:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
agent.clear_transcript!
|
|
122
|
+
agent.clear_knowledge!
|
|
123
|
+
agent.reset_context!
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`purge!` is different: it permanently removes the Agent and persisted execution
|
|
127
|
+
history from the configured Persistence backend.
|
|
128
|
+
|
|
129
|
+
## Sync and async Agent APIs
|
|
130
|
+
|
|
131
|
+
Use synchronous APIs at an external/top-level application boundary and async
|
|
132
|
+
APIs when the caller must remain non-blocking.
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
result = agent.invoke("Hello")
|
|
136
|
+
|
|
137
|
+
task = agent.invoke_async("Hello")
|
|
138
|
+
result = task.wait_result
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`Task#wait_result` is for an external caller. Do not block EventLoop waiting for
|
|
142
|
+
a Task that can only complete through that same EventLoop.
|
|
143
|
+
|
|
144
|
+
Streaming follows the same split:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
agent.stream("Explain the design") do |event|
|
|
148
|
+
puts event.payload if event.type == :token
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
task = agent.stream_async(
|
|
154
|
+
"Explain the design",
|
|
155
|
+
on_event: ->(event) { puts event.payload if event.type == :token }
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Streaming callbacks execute on EventLoop and therefore should return quickly.
|
|
160
|
+
|
|
161
|
+
## Human-in-the-loop approval
|
|
162
|
+
|
|
163
|
+
A Tool requiring approval can suspend an Agent invocation. Resume it with the
|
|
164
|
+
approval request identifier returned by the suspension result.
|
|
165
|
+
|
|
166
|
+
At a top-level synchronous boundary:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
result = agent.invoke("Perform the requested protected action")
|
|
170
|
+
|
|
171
|
+
if result[:suspended]
|
|
172
|
+
request = result[:approval_request]
|
|
173
|
+
result = agent.approve(
|
|
174
|
+
result[:execution_id],
|
|
175
|
+
approval_request_id: request.id,
|
|
176
|
+
approved: true
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
From an EventLoop callback, use `approve_async` rather than blocking EventLoop.
|
|
182
|
+
Approval resume continues the same live Agent instance, Activation, and
|
|
183
|
+
AgentInvocation; it is not an Agent reload boundary.
|
|
184
|
+
|
|
185
|
+
## Workflow basics
|
|
186
|
+
|
|
187
|
+
A Workflow is state-machine-driven and can halt at an explicit wait state:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class ReviewContext
|
|
191
|
+
include Phronomy::WorkflowContext
|
|
192
|
+
field :draft, type: :replace
|
|
193
|
+
field :feedback, type: :replace
|
|
194
|
+
field :approved, type: :replace, default: false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
write_draft = ->(state) { state.merge(draft: "Draft content") }
|
|
198
|
+
review_draft = ->(state) { state.merge(feedback: "Feedback on: #{state.draft}") }
|
|
199
|
+
|
|
200
|
+
persistence = Phronomy::Persistence::InMemory.new
|
|
201
|
+
|
|
202
|
+
workflow = Phronomy::Workflow.define(
|
|
203
|
+
ReviewContext,
|
|
204
|
+
persistence: persistence
|
|
205
|
+
) do
|
|
206
|
+
initial :write
|
|
207
|
+
state :write, action: write_draft
|
|
208
|
+
state :review, action: review_draft
|
|
209
|
+
wait_state :awaiting_approval
|
|
210
|
+
state :finalize, action: ->(s) { s.merge(approved: true) }
|
|
211
|
+
|
|
212
|
+
transition from: :write, to: :review
|
|
213
|
+
transition from: :review, to: :awaiting_approval
|
|
214
|
+
transition from: :awaiting_approval, on: :approve, to: :finalize
|
|
215
|
+
transition from: :awaiting_approval, on: :reject, to: :write
|
|
216
|
+
transition from: :finalize, to: :__finish__
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
state = workflow.invoke({draft: ""}, config: {thread_id: "doc-1"})
|
|
220
|
+
final = workflow.send_event(state: state, event: :approve)
|
|
221
|
+
puts final.approved
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`Persistence#workflow_states` is the durable Workflow repository. `thread_id`
|
|
225
|
+
identifies the durable Workflow state and remains stable across resume. Each
|
|
226
|
+
concrete Runtime execution receives a separate internal `fsm_session_id`; the
|
|
227
|
+
application-level `session_id` remains ordinary caller/tracing metadata. Phronomy
|
|
228
|
+
holds owner-aware admission for `thread_id` from durable load through terminal
|
|
229
|
+
save so another local invocation cannot start from a stale snapshot while the
|
|
230
|
+
current owner is still committing.
|
|
231
|
+
|
|
232
|
+
A global Persistence backend can be configured when Agents and Workflows should
|
|
233
|
+
share one durable backend:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
Phronomy.configure do |config|
|
|
237
|
+
config.persistence = persistence
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Agent `new`/`create` and Workflow definitions use the global backend when they do
|
|
242
|
+
not inject an explicit `persistence:`. Workflow durability is fixed at the
|
|
243
|
+
application/Workflow-definition boundary; there is no per-invocation backend
|
|
244
|
+
switch.
|
|
245
|
+
|
|
246
|
+
Workflow entry and transition actions are synchronous Run-to-Completion
|
|
247
|
+
callbacks. If a Workflow needs an Agent or another asynchronous lifecycle, start
|
|
248
|
+
it asynchronously, return the Workflow context immediately, and deliver its
|
|
249
|
+
completion later with `Workflow#signal`.
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
class AnswerContext
|
|
253
|
+
include Phronomy::WorkflowContext
|
|
254
|
+
|
|
255
|
+
field :question, type: :replace, default: ""
|
|
256
|
+
field :answer, type: :replace, default: nil
|
|
257
|
+
field :thread_id, type: :replace, default: nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
class ResearchAgent < Phronomy::Agent::Base
|
|
261
|
+
agent_definition id: "research-agent", version: 1
|
|
262
|
+
model "gpt-4o-mini"
|
|
263
|
+
instructions "Research the question and return a concise answer."
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
my_agent = ResearchAgent.new
|
|
267
|
+
workflow = nil
|
|
268
|
+
|
|
269
|
+
workflow = Phronomy::Workflow.define(AnswerContext) do
|
|
270
|
+
initial :asking
|
|
271
|
+
state :asking
|
|
272
|
+
state :done
|
|
273
|
+
|
|
274
|
+
entry :asking, ->(ctx) {
|
|
275
|
+
thread_id = ctx.thread_id
|
|
276
|
+
|
|
277
|
+
my_agent.invoke_async(
|
|
278
|
+
ctx.question,
|
|
279
|
+
on_event: ->(event) {
|
|
280
|
+
next unless event.type == :done
|
|
281
|
+
|
|
282
|
+
workflow.signal(
|
|
283
|
+
thread_id: thread_id,
|
|
284
|
+
event: :answer_ready,
|
|
285
|
+
payload: {answer: event.payload[:output]}
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
ctx
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
transition(
|
|
294
|
+
from: :asking,
|
|
295
|
+
on: :answer_ready,
|
|
296
|
+
to: :done,
|
|
297
|
+
action: ->(ctx, event) { ctx.merge(answer: event.payload[:answer]) }
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Returning a `Phronomy::Task` from a Workflow entry/transition action is not an
|
|
303
|
+
implicit await mechanism and is rejected.
|
|
304
|
+
|
|
305
|
+
## Agent as Tool
|
|
306
|
+
|
|
307
|
+
Expose a child Agent using `Phronomy::Tools::Agent.from_agent` rather than calling
|
|
308
|
+
a synchronous child Agent from a Tool worker:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
ResearchTool = Phronomy::Tools::Agent.from_agent(
|
|
312
|
+
ResearchAgent,
|
|
313
|
+
tool_name: "research",
|
|
314
|
+
description: "Delegate research to the research Agent"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
class OrchestratorAgent < Phronomy::Agent::Base
|
|
318
|
+
agent_definition id: "orchestrator-agent", version: 1
|
|
319
|
+
model "gpt-4o"
|
|
320
|
+
instructions "Use the research Tool when research is required."
|
|
321
|
+
tools(ResearchTool => nil)
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Agent-backed Tools return control to EventLoop while the child lifecycle is
|
|
326
|
+
waiting. They do not occupy an OffloadPool worker merely to wait for a child
|
|
327
|
+
Agent result.
|
|
328
|
+
|
|
329
|
+
## Filters
|
|
330
|
+
|
|
331
|
+
Filters can transform or reject values at Agent boundaries:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
class NoCreditCardFilter < Phronomy::Filter::Base
|
|
335
|
+
def call(value, **_context)
|
|
336
|
+
block!("Credit card numbers are not allowed") if value.match?(/\d{4}-\d{4}-\d{4}-\d{4}/)
|
|
337
|
+
value
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
agent.add_input_filter(NoCreditCardFilter.new)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Phronomy includes `PromptInjectionFilter` as a baseline pattern filter. It is not
|
|
345
|
+
a complete security policy for untrusted input.
|
|
346
|
+
|
|
347
|
+
## Persistent Knowledge and per-call context
|
|
348
|
+
|
|
349
|
+
Register durable Knowledge on the Agent:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
agent.add_knowledge(
|
|
353
|
+
"Customer locale: ja-JP",
|
|
354
|
+
metadata: {"origin" => "customer_profile"}
|
|
355
|
+
)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Request-scoped context can instead be supplied through `before_llm_input` using
|
|
359
|
+
`LLMInputPatch#segment_candidates`; those candidates enter Context Policy for the
|
|
360
|
+
specific call and are not persisted to the Journal.
|
|
361
|
+
|
|
362
|
+
## Model Context Protocol (MCP)
|
|
363
|
+
|
|
364
|
+
Phronomy targets MCP 1.x through the official `mcp` gem:
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
search_tool = Phronomy::Tools::Mcp.from_server(
|
|
368
|
+
"stdio://./mcp-server",
|
|
369
|
+
tool_name: "web_search"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
begin
|
|
373
|
+
# use search_tool
|
|
374
|
+
ensure
|
|
375
|
+
search_tool.close
|
|
376
|
+
end
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
See [MCP client](mcp-client.md) for schema, error, cancellation, and lifecycle
|
|
380
|
+
contracts.
|
|
381
|
+
|
|
382
|
+
## Next steps
|
|
383
|
+
|
|
384
|
+
- [Features and API stability](features.md)
|
|
385
|
+
- [Runtime and concurrency](runtime-and-concurrency.md)
|
|
386
|
+
- [Architecture decisions](decisions/)
|
|
387
|
+
- [Migration guides](migrations/)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Migration from 0.15-era execution-policy APIs
|
|
2
|
+
|
|
3
|
+
This guide preserves the execution-policy migration information that previously
|
|
4
|
+
lived in the top-level README. It is historical upgrade guidance; new code should
|
|
5
|
+
follow the current contracts documented in
|
|
6
|
+
[Runtime and concurrency](../runtime-and-concurrency.md).
|
|
7
|
+
|
|
8
|
+
| Removed Phronomy setting/API | Replacement |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `retry_policy` | RubyLLM transport retry, or explicit application orchestration |
|
|
11
|
+
| `invoke_timeout` | `InvocationContext#deadline` or `cancellation_token` when the caller needs a root deadline |
|
|
12
|
+
| `config[:llm_timeout]` | `RubyLLM.configure { |c| c.request_timeout = ... }` |
|
|
13
|
+
| Tool `retry_on` | Tool/client-specific retry with explicit idempotency guarantees |
|
|
14
|
+
| `config[:tool_timeout]` | Tool/client-native timeout |
|
|
15
|
+
| `max_parallel_tools` | No replacement; `parallel_tool_execution` remains an on/off mode |
|
|
16
|
+
| `InvocationContext#provider_limits` | Configure the provider client directly |
|
|
17
|
+
| `stream_queue_max_size` | No replacement; the shared EventLoop queue is unbounded by design. Monitor `Metrics.snapshot[:event_loop_queue_depth]` instead |
|
|
18
|
+
|
|
19
|
+
## Timeout ownership
|
|
20
|
+
|
|
21
|
+
Phronomy owns logical lifecycle deadlines and cooperative cancellation. Provider,
|
|
22
|
+
HTTP, database, MCP, and other transport-level timeouts belong to the relevant
|
|
23
|
+
client library.
|
|
24
|
+
|
|
25
|
+
A caller-defined root deadline can be represented through `InvocationContext` or
|
|
26
|
+
a cancellation token, but that does not replace operation-native socket/request/
|
|
27
|
+
statement timeouts for a synchronous call already in progress.
|
|
28
|
+
|
|
29
|
+
## Retry ownership
|
|
30
|
+
|
|
31
|
+
Automatic replay of an entire Agent invocation is not a generic framework retry
|
|
32
|
+
policy. Configure transport retry on RubyLLM or another adapter, and implement
|
|
33
|
+
application orchestration explicitly when replay is semantically safe.
|
|
34
|
+
|
|
35
|
+
See [`CHANGELOG.md`](../../CHANGELOG.md) for the release-by-release history.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 0.16 cleanup migration
|
|
2
|
+
|
|
3
|
+
This guide preserves the compatibility cleanup information that previously lived
|
|
4
|
+
in the top-level README. It is intended for applications upgrading older Phronomy
|
|
5
|
+
code; new applications should use the current documentation directly.
|
|
6
|
+
|
|
7
|
+
| Removed API | Current contract |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `context_overhead` | Manifest-first assembly budgets actual mandatory + selected content |
|
|
10
|
+
| Tool `on_error :return_empty` | Use `:raise` or `:suppress` |
|
|
11
|
+
| `dispatch_parallel(..., force_kill:)` / `fan_out(..., force_kill:)` | Cooperative cancellation; no force-kill switch |
|
|
12
|
+
| `runtime_backend` | Removed. Phronomy has one control model: EventLoop/FSMSession for lifecycle coordination and OffloadPool for synchronous work that must stay off EventLoop |
|
|
13
|
+
| `Runtime.instance = ...` | Runtime replacement is test/internal infrastructure, not a public setter |
|
|
14
|
+
| `Runtime#spawn` / `TaskGroup` | Removed. Start framework async work through domain async APIs (`invoke_async`, Workflow events, ToolInvocation, FanOut) |
|
|
15
|
+
| `tools ToolA, ToolB` | Use `tools(ToolA => nil, ToolB => nil)` |
|
|
16
|
+
| `CancellationToken.new(deadline: Time...)` | Use `CancellationToken.timeout_after(seconds)` for a lazy token deadline, or a Runtime-backed cancellation scope when callback delivery on deadline is required |
|
|
17
|
+
| `StaticKnowledge` / `EntityKnowledge` / `Knowledge::Base` / `Phronomy::KnowledgeSource` | Register plain persistent Knowledge with `knowledge:` or `add_knowledge` |
|
|
18
|
+
| `static_knowledge*` class APIs | Persistent Knowledge belongs to Agent instances and is Journal-backed |
|
|
19
|
+
| `clear_memory!` | Use `clear_knowledge!`; conversation history is controlled independently with `clear_transcript!` |
|
|
20
|
+
|
|
21
|
+
The legacy `build_context` / `LlmContextWindow::Assembler` extension path is no
|
|
22
|
+
longer an active API. Stateful Agent input is assembled through the canonical:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Journal
|
|
26
|
+
→ Context candidates
|
|
27
|
+
→ Context Policy
|
|
28
|
+
→ Large Language Model (LLM) Input Manifest
|
|
29
|
+
→ runtime projection
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Concurrency model
|
|
33
|
+
|
|
34
|
+
The active concurrency model is EventLoop/FSMSession for logical lifecycle
|
|
35
|
+
coordination plus OffloadPool for synchronous work that cannot run on EventLoop.
|
|
36
|
+
There is no configurable runtime backend and no generic framework task-spawn
|
|
37
|
+
backend.
|
|
38
|
+
|
|
39
|
+
For the current design, see
|
|
40
|
+
[Runtime and concurrency](../runtime-and-concurrency.md) and
|
|
41
|
+
[ADR-010](../decisions/010-cooperative-first-concurrency.md).
|
|
42
|
+
|
|
43
|
+
See [`CHANGELOG.md`](../../CHANGELOG.md) for release details.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Migrating to 0.19: Unified Persistence
|
|
2
|
+
|
|
3
|
+
This release removes the separate Workflow `StateStore` abstraction and uses
|
|
4
|
+
`Phronomy::Persistence` for both Agent and Workflow durable state.
|
|
5
|
+
|
|
6
|
+
## Workflow persistence
|
|
7
|
+
|
|
8
|
+
Before:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
store = Phronomy::StateStore::InMemory.new
|
|
12
|
+
|
|
13
|
+
workflow = Phronomy::Workflow.define(
|
|
14
|
+
MyContext,
|
|
15
|
+
state_store: store
|
|
16
|
+
) do
|
|
17
|
+
# ...
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
After:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
persistence = Phronomy::Persistence::InMemory.new
|
|
25
|
+
|
|
26
|
+
workflow = Phronomy::Workflow.define(
|
|
27
|
+
MyContext,
|
|
28
|
+
persistence: persistence
|
|
29
|
+
) do
|
|
30
|
+
# ...
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Global configuration changes from:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Phronomy.configure do |config|
|
|
38
|
+
config.state_store = store
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
to:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Phronomy.configure do |config|
|
|
46
|
+
config.persistence = persistence
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The global backend is also used by Agent `new`/`create` calls that do not pass an
|
|
51
|
+
explicit `persistence:`. An explicitly injected Agent or Workflow backend still
|
|
52
|
+
takes precedence over the global setting.
|
|
53
|
+
|
|
54
|
+
Per-invocation `config[:state_store]` is removed. Persistence is selected at the
|
|
55
|
+
Workflow definition/application boundary rather than switched on individual
|
|
56
|
+
invocations.
|
|
57
|
+
|
|
58
|
+
`Phronomy::StateStore`, `StateStore::InMemory`, and the `state_store:` Workflow
|
|
59
|
+
parameter are removed without compatibility aliases.
|
|
60
|
+
|
|
61
|
+
## workflow_states repository
|
|
62
|
+
|
|
63
|
+
Custom Persistence backends must provide `workflow_states` with revision-aware
|
|
64
|
+
load/save/delete semantics:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
record = persistence.workflow_states.load("order-123")
|
|
68
|
+
|
|
69
|
+
persistence.workflow_states.save(
|
|
70
|
+
"order-123",
|
|
71
|
+
expected_revision: record&.fetch(:revision),
|
|
72
|
+
snapshot: {
|
|
73
|
+
fields: workflow_context.to_h,
|
|
74
|
+
phase: workflow_context.phase.to_s
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A stale revision must raise `Phronomy::Persistence::ConflictError` rather than
|
|
80
|
+
silently overwriting newer durable state.
|
|
81
|
+
|
|
82
|
+
Custom durable backends must also implement the internal
|
|
83
|
+
`Persistence#assert_agent_watermark!` precondition used before next-LLM durable
|
|
84
|
+
barriers. It verifies the caller's Agent revision and Journal position without
|
|
85
|
+
returning replacement mutable state; mismatches raise `ConflictError`. SQL
|
|
86
|
+
backends should implement this check inside the same database transaction as the
|
|
87
|
+
subsequent durable write.
|
|
88
|
+
|
|
89
|
+
## Agent ownership and approval resume
|
|
90
|
+
|
|
91
|
+
A live Agent instance now remains the logical owner of its mutable state after
|
|
92
|
+
hydration. Phronomy no longer re-loads mutable AgentRoot/AgentExecution/Journal
|
|
93
|
+
state at each LLM boundary just to obtain freshness.
|
|
94
|
+
|
|
95
|
+
Approval resume continues the same live Agent/Activation and is performed through
|
|
96
|
+
the Agent instance APIs:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
agent.approve(...)
|
|
100
|
+
agent.approve_async(...)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The former class-level `MyAgent.approve(...)` / `MyAgent.approve_async(...)`
|
|
104
|
+
routing APIs are removed. Applications that only have an `execution_id` should
|
|
105
|
+
first resolve the current process's live owner:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
agent = MyAgent.live_for_execution(execution_id)
|
|
109
|
+
|
|
110
|
+
agent.approve_async(
|
|
111
|
+
execution_id,
|
|
112
|
+
approval_request_id: request_id
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If the concrete Agent class is not known, use:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
agent = Phronomy::Agent::Base.live_for_execution(execution_id)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`live_for_execution` resolves the Runtime-local Activation and returns its
|
|
123
|
+
existing Agent instance. It does not accept `persistence:` and does not load a new
|
|
124
|
+
Agent or Execution from Persistence. If the live Activation has been lost (for
|
|
125
|
+
example after process restart), it raises `ExecutionRehydrationRequiredError`
|
|
126
|
+
until durable execution rehydration is implemented.
|
|
127
|
+
|
|
128
|
+
`execution_id` is not an authorization token. HTTP/API applications must still
|
|
129
|
+
verify that the caller is allowed to act on the resolved Agent and approval
|
|
130
|
+
request.
|
|
131
|
+
|
|
132
|
+
## Workflow identity
|
|
133
|
+
|
|
134
|
+
Do not treat the following as the same identifier:
|
|
135
|
+
|
|
136
|
+
- `session_id`: application/tracing session metadata;
|
|
137
|
+
- `thread_id`: durable Workflow state identity;
|
|
138
|
+
- `fsm_session_id`: private Runtime identity for one FSMSession execution.
|
|
139
|
+
|
|
140
|
+
The last value is internal and is regenerated for each invoke/resume. Applications
|
|
141
|
+
continue to use `thread_id` when calling `Workflow#signal`.
|
|
142
|
+
|
|
143
|
+
Workflow admission for a durable `thread_id` is Runtime/process-local. Separate
|
|
144
|
+
Ruby processes or service replicas can execute the same `thread_id` concurrently
|
|
145
|
+
unless the application adds distributed coordination. `workflow_states` optimistic
|
|
146
|
+
revisions can reject a stale terminal commit, but they do not prevent duplicate
|
|
147
|
+
execution from starting or undo external side effects already performed before a
|
|
148
|
+
conflict is detected.
|