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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -1134
  3. data/README.md +70 -1280
  4. data/benchmark/bench_regression.rb +25 -5
  5. data/docs/changelog/0.14-and-earlier.md +1137 -0
  6. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  7. data/docs/decisions/009-state-store-abstraction.md +1 -1
  8. data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
  9. data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
  10. data/docs/features.md +93 -0
  11. data/docs/getting-started.md +387 -0
  12. data/docs/migrations/0.15.md +35 -0
  13. data/docs/migrations/0.16.md +43 -0
  14. data/docs/migrations/0.19.md +148 -0
  15. data/docs/runtime-and-concurrency.md +349 -0
  16. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  17. data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
  18. data/lib/phronomy/agent/async_event_api.rb +3 -3
  19. data/lib/phronomy/agent/base.rb +116 -55
  20. data/lib/phronomy/agent/context/capability/base.rb +13 -3
  21. data/lib/phronomy/agent/context_assembler.rb +13 -3
  22. data/lib/phronomy/agent/execution_coordinator.rb +423 -252
  23. data/lib/phronomy/agent/journal_projection.rb +5 -1
  24. data/lib/phronomy/agent/shared_state.rb +2 -0
  25. data/lib/phronomy/agent/tool_executor.rb +29 -71
  26. data/lib/phronomy/agent/tool_invocation.rb +97 -47
  27. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  28. data/lib/phronomy/configuration.rb +7 -30
  29. data/lib/phronomy/diagnostics.rb +12 -41
  30. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  31. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  32. data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
  33. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  34. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  35. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  36. data/lib/phronomy/engine/event_loop.rb +170 -193
  37. data/lib/phronomy/engine/fsm_session.rb +6 -4
  38. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  39. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  40. data/lib/phronomy/engine/runtime.rb +52 -158
  41. data/lib/phronomy/engine/task.rb +136 -277
  42. data/lib/phronomy/llm_adapter/base.rb +14 -14
  43. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  44. data/lib/phronomy/llm_adapter.rb +2 -2
  45. data/lib/phronomy/metrics.rb +15 -30
  46. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  47. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  48. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  49. data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
  50. data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
  51. data/lib/phronomy/persistence/in_memory.rb +113 -8
  52. data/lib/phronomy/persistence.rb +12 -3
  53. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  54. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  55. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  56. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  57. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  58. data/lib/phronomy/testing/eval/runner.rb +52 -0
  59. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  60. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  61. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  62. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  63. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  64. data/lib/phronomy/testing/eval.rb +9 -0
  65. data/lib/phronomy/testing/fake_clock.rb +6 -53
  66. data/lib/phronomy/testing.rb +2 -6
  67. data/lib/phronomy/tools/agent.rb +141 -6
  68. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  69. data/lib/phronomy/vector_store/base.rb +2 -2
  70. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  71. data/lib/phronomy/version.rb +1 -1
  72. data/lib/phronomy/workflow.rb +10 -9
  73. data/lib/phronomy/workflow_runner.rb +363 -99
  74. data/lib/phronomy.rb +10 -4
  75. data/scripts/api_snapshot.rb +4 -4
  76. metadata +25 -33
  77. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  78. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  79. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  80. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  81. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  82. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  83. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  84. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  85. data/lib/phronomy/engine/task/backend.rb +0 -80
  86. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  87. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  88. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  89. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  90. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  91. data/lib/phronomy/engine/task_group.rb +0 -193
  92. data/lib/phronomy/eval/comparison.rb +0 -47
  93. data/lib/phronomy/eval/dataset.rb +0 -45
  94. data/lib/phronomy/eval/eval_case.rb +0 -17
  95. data/lib/phronomy/eval/eval_result.rb +0 -29
  96. data/lib/phronomy/eval/metrics.rb +0 -66
  97. data/lib/phronomy/eval/runner.rb +0 -94
  98. data/lib/phronomy/eval/scorer/base.rb +0 -22
  99. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  100. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  101. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  102. data/lib/phronomy/eval/scorer.rb +0 -9
  103. data/lib/phronomy/eval.rb +0 -7
  104. data/lib/phronomy/state_store/base.rb +0 -48
  105. data/lib/phronomy/state_store/in_memory.rb +0 -62
  106. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  107. 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.