phronomy 0.16.0 → 0.18.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -0,0 +1,1137 @@
1
+ # Changelog archive: 0.14.0 and earlier
2
+
3
+ This file contains release history moved from the top-level `CHANGELOG.md` to keep
4
+ the active changelog focused on unreleased work and recent releases. The entries
5
+ below are historical and intentionally retain the terminology and APIs that were
6
+ current at the time of each release.
7
+
8
+ ## [0.14.0] - 2026-07-27
9
+
10
+ ### Changed
11
+
12
+ - **Runtime-owned EventLoop lifecycle** (`refactor: Runtime-owned EventLoop lifecycle`):
13
+ - `Runtime#event_loop` lazy accessor replaces the standalone `EventLoop.instance` singleton.
14
+ - `EventLoop.instance` is retained as a deprecated compatibility bridge to `Runtime.instance.event_loop`.
15
+ - EventLoop dispatcher now runs in a dedicated real-thread service scheduler owned by `Runtime`,
16
+ removing the hidden `Runtime.new(scheduler: ThreadScheduler.new)` inside `EventLoop`.
17
+ - EventLoop lifecycle is terminal: `:running` → `:draining` → `:stopping` → `:terminated` / `:failed`.
18
+ - `STOP` Object sentinel replaces the `:__stop__` symbol; stale sentinel handling is removed.
19
+ - `@running` instance variable removed; dispatch loop driven solely by the STOP sentinel.
20
+ - EventLoop-specific `@shutdown_token` removed.
21
+ - `outstanding_sessions` counter covers queue-pending `:start` events from `register` time,
22
+ ensuring drain correctness when shutdown races with queued sessions.
23
+ - Shutdown timeout: `AbortAndStop` control command path plus `cancel!` → `join(cancel_grace)` fallback.
24
+ - `Runtime.reset_default!` replaces `EventLoop.reset!`; `Phronomy.reset_runtime!` performs real Runtime shutdown then config reset.
25
+ - `ShutdownResult` value object separates `runtime_outcome` from `cleanup_status`.
26
+ - `FSMSession` now receives `event_loop:` and `timer_queue_provider:` explicitly at build time.
27
+ - Agent / Workflow phase-machine builders capture the owning EventLoop at invocation time;
28
+ `EventLoop.instance` is no longer re-fetched inside async callbacks or timers.
29
+
30
+ ### Fixed
31
+
32
+ - **Double-dispatch regression (P0 hotfix — commit `2cc8c6c1`)**: `EventLoop#stop` no longer clears `@task`
33
+ while the dispatcher task is still alive. A subsequent `start` call detects the live task and does
34
+ not spawn a second dispatch loop on the same queue. Includes `:cancel_timeout` status for the case
35
+ where `cancel!` does not terminate the task within `cancel_grace` seconds.
36
+ - **`EventLoop.reset!` safety**: raises `Phronomy::Error` when the dispatcher task is still alive
37
+ after stop instead of unconditionally clearing the singleton.
38
+ - **`EventLoop#task_alive?`**: new public helper; thread-safe boolean for use in reset logic.
39
+
40
+
41
+ `BlockingAdapterPool#submit` previously stored the timeout value but never
42
+ registered a wall-clock timer, so `config: { llm_timeout: N }` and
43
+ `config: { tool_timeout: N }` had no effect for callers using `on_complete`
44
+ (the normal non-streaming Agent path). The timer is now armed before queue
45
+ admission and calls `fire_timeout!` when the deadline expires.
46
+
47
+ ### Changed
48
+
49
+ - **`blocking_wait(timeout:)` is now a waiter-local deadline only**:
50
+ Previously, the timeout passed to `blocking_wait` (or `wait_result`) would settle the
51
+ operation, set `abandoned? = true`, and increment `abandoned_count` — affecting
52
+ all future waiters and callbacks. It is now scoped to the single calling thread:
53
+ the caller receives `TimeoutError`, but the operation remains unsettled. Other
54
+ waiters or `on_complete` callbacks will still receive the eventual result unless
55
+ a separate submit-time deadline or cancellation settles the operation first.
56
+ **Callers that relied on `blocking_wait(timeout:)` to abandon and count an
57
+ operation must switch to a submit-time `timeout:` passed to `pool.submit`.**
58
+
59
+ - **Queue-timeout operations are not counted as abandoned**:
60
+ When a submit-time timeout fires before the worker picks up the operation,
61
+ the operation is settled with `TimeoutError` but `abandoned? == false` and
62
+ `abandoned_count` is not incremented. Only timeouts that fire while the block
63
+ is executing set `abandoned? = true`.
64
+
65
+ - **MCP client support now requires `mcp` 1.x**:
66
+ The `mcp` SDK 0.x dependency is no longer supported. The constraint is now
67
+ `mcp ~> 1.0`. `faraday` and `event_stream_parser` are added as direct runtime
68
+ dependencies so HTTP/SSE transport works without relying on transitive
69
+ resolution through RubyLLM.
70
+
71
+ - **MCP Tool error handling follows MCP 1.x semantics**:
72
+ JSON-RPC errors (`MCP::Client::ServerError`) are converted to
73
+ `Phronomy::ToolError`. Cancellations (`MCP::CancelledError`) are converted to
74
+ `Phronomy::CancellationError`. Tool-level `isError: true` results are
75
+ returned to the model as error text rather than raising, allowing the LLM to
76
+ self-correct.
77
+
78
+ - **MCP input schemas now use a strict supported subset of JSON Schema 2020-12**:
79
+ Unsupported structural keywords (`oneOf`, `anyOf`, `allOf`, `$ref`, etc.),
80
+ nested object/array types, and nullable type arrays fail fast with
81
+ `Phronomy::ToolError` at `from_server` time. Constraint-only annotations
82
+ (`minimum`, `maxLength`, `format`, etc.) produce a logger warning and are
83
+ otherwise ignored. See [`docs/mcp-client.md`](../mcp-client.md) for the
84
+ full supported schema subset.
85
+
86
+ - **MCP client cancellation now invalidates the transport**:
87
+ After a `MCP::CancelledError` the internal client reference is set to `nil`
88
+ and the old transport is closed asynchronously. The next tool call creates a
89
+ fresh connection, preventing stdio response mis-routing from a lingering SDK
90
+ worker thread.
91
+
92
+ ---
93
+
94
+ ## [0.13.0] - 2026-07-23
95
+
96
+ ### Added
97
+
98
+ - **`Phronomy::Filter::Base` — unified value filter interface** (#389):
99
+ A single abstract base class `Filter::Base` with one method `call(value, **context)`
100
+ covers all three agent boundaries — user input, LLM output, and tool return values.
101
+ Subclasses return the (possibly transformed) value to continue, or call `block!` /
102
+ `raise Phronomy::FilterBlockError` to reject. The same filter instance can be
103
+ registered at multiple sites. Guardrails registered via `add_input_guardrail` /
104
+ `add_output_guardrail` are automatically included at the front of the filter chain.
105
+
106
+ ```ruby
107
+ class PiiMaskFilter < Phronomy::Filter::Base
108
+ def call(value, **_context)
109
+ value.to_s.gsub(/\b\d{2,4}-\d{2,4}-\d{4}\b/, "[PHONE]")
110
+ end
111
+ end
112
+
113
+ f = PiiMaskFilter.new
114
+ agent.add_input_filter(f)
115
+ agent.add_output_filter(f)
116
+ agent.add_tool_result_filter(CustomerDataTool, f)
117
+ ```
118
+
119
+ Class-level DSL counterparts: `input_filter`, `output_filter`, `tool_result_filter`.
120
+ The `tools(Hash)` DSL also accepts a `:result_filter` key for per-tool scoping.
121
+
122
+ - **`Guardrail::Base#call` — Filter::Base-compatible interface**:
123
+ `Guardrail::Base` now implements `call(value, **_context)`, which calls `check(value)`
124
+ and returns the value unchanged (or raises `GuardrailError`). Existing guardrail
125
+ subclasses require no changes.
126
+
127
+ ### Changed
128
+
129
+ - **Guardrail execution unified into the filter chain**:
130
+ Guardrails registered via `add_input_guardrail` / `add_output_guardrail` now run
131
+ as the first entries in `run_input_filters!` / `run_output_filters!`. The separate
132
+ `run_input_guardrails!` / `run_output_guardrails!` call sites in `invoke_once`,
133
+ `_stream_impl`, and `Suspendable#resume` have been removed. Behaviour is unchanged
134
+ — guardrails still run before any filters and `GuardrailError` still propagates.
135
+
136
+ ### Changed
137
+
138
+ - **MCP transport replaced by official `mcp` gem** (closes #280, #365):
139
+ The hand-rolled `StdioTransport` and `HttpTransport` (~260 lines) have been
140
+ replaced by `MCP::Client::Stdio` and `MCP::Client::HTTP` from the official
141
+ `mcp` gem (v0.25.0+). Adds the `mcp >= 0.3` runtime dependency.
142
+ - Built-in 4 MiB size limits for stdout, stderr, and HTTP responses
143
+ - Automatic MCP `initialize` handshake on every connection
144
+ - SSE response parsing handled by the SDK
145
+ - `from_server` and `execute` convert SDK errors into `Phronomy::ToolError`
146
+ - Removes all `Thread.new` usage from `mcp.rb`; entry removed from
147
+ `THREAD_NEW_ALLOWLIST` in `thread_invariants_spec.rb`
148
+
149
+ - **`CancellationToken` bridged into MCP `call_tool`** (Issue #390):
150
+ `Mcp#execute` now accepts a `cancellation_token:` keyword argument.
151
+ When provided, the token is bridged to `MCP::Cancellation` via `on_cancel`,
152
+ so an explicit `cancel!` propagates into the in-flight `call_tool` request
153
+ as a MCP `notifications/cancelled` message.
154
+
155
+ - **`config[:tool_timeout]` forwarded to `BlockingAdapterPool`** (Issue #390):
156
+ `ToolExecutor.call_async` and `Capability::Base#call_async` now accept a
157
+ `config:` keyword. `config[:tool_timeout]` is passed as the `timeout:` to
158
+ `pool.submit`, enabling the same abandoned-operation tracking that LLM calls
159
+ already use via `config[:llm_timeout]`.
160
+
161
+
162
+
163
+ ## [0.10.0] - 2026-06-08
164
+
165
+ ### Added
166
+
167
+ - **`Task#map` — transform a Task's completed value** (#384):
168
+ `task.map { |result| ctx.merge(answer: result[:output]) }` returns a new `Task`
169
+ whose completed value is the block's return value. Primary use-case: wire
170
+ `Agent::Base#invoke_async` into a Workflow entry action so the agent result
171
+ reaches `WorkflowContext` through the standard `:action_completed` path without
172
+ requiring any changes to `FSMSession`. If the source task fails or is cancelled,
173
+ the mapped task propagates the error without calling the block.
174
+
175
+ ### Removed
176
+
177
+ - **`Agent::Base#run_as_child` removed** (#384):
178
+ Introduced when agents had their own FSM (`Agent::FSM`). After `Agent::FSM` was
179
+ removed in v0.9.0, the `:child_completed` event payload was silently discarded by
180
+ `FSMSession`, so `ctx.answer` was never populated. Migrate to
181
+ `invoke_async + Task#map`:
182
+
183
+ ```ruby
184
+ # Before (removed)
185
+ entry :translate, ->(ctx) { TranslationAgent.new.run_as_child(ctx.query, ctx: ctx) }
186
+ transition from: :translate, on: :child_completed, to: :done
187
+
188
+ # After (recommended)
189
+ entry :translate, ->(ctx) {
190
+ TranslationAgent.new.invoke_async(ctx.query).map { |r| ctx.merge(answer: r[:output]) }
191
+ }
192
+ transition from: :translate, to: :done
193
+ ```
194
+
195
+ ### Added
196
+
197
+ - **`Task#map` — transform a Task's completed value** (post-v0.9.0):
198
+ `task.map { |result| ctx.merge(answer: result[:output]) }` returns a new `Task`
199
+ whose completed value is the block's return value. The primary use-case is
200
+ connecting `Agent::Base#invoke_async` to a Workflow entry action so the agent
201
+ result populates the `WorkflowContext` through the standard `:action_completed`
202
+ path. If the source task fails or is cancelled, the mapped task propagates the
203
+ error without calling the block.
204
+
205
+ - **`Phronomy::Diagnostics` and `SchedulerReentrancyError`** (#278, #279):
206
+ `Phronomy::Diagnostics` exposes a snapshot of current scheduler state
207
+ (`pending_count`, `active_tasks`, `pool_utilization`, etc.) for debugging and
208
+ monitoring. `SchedulerReentrancyError` is raised when a scheduler operation is
209
+ attempted from within a scheduler callback, preventing deadlocks.
210
+ `Phronomy.configure { |c| c.scheduler_debug = true }` enables verbose scheduler
211
+ logging.
212
+
213
+ - **`task_id` / `parent_task_id` on `InvocationContext`** (#277):
214
+ Every task spawned via `Task.spawn` now carries a `task_id` (a random UUID) and
215
+ an optional `parent_task_id`. These fields enable hierarchical task-tree tracing
216
+ and are forwarded automatically by `TaskGroup`.
217
+
218
+ - **`Phronomy::Metrics` — task-centric observability snapshot** (#276):
219
+ `Phronomy::Metrics.snapshot` returns a hash with scheduler statistics:
220
+ `tasks_started`, `tasks_completed`, `tasks_failed`, `pool_queue_depth`, and
221
+ `pool_active_threads`. Intended for metrics export and health-check endpoints.
222
+
223
+ - **`Phronomy::Testing::FakeClock` and `FakeScheduler`** (#273):
224
+ Two test helpers for deterministic concurrency testing.
225
+ `FakeClock` exposes `advance(seconds)` to control the passage of time without
226
+ sleeping. `FakeScheduler` replaces the real scheduler in specs, providing
227
+ synchronous execution and `flush` / `drain` helpers to drive task completion.
228
+
229
+ - **`ScopePolicy` and approval gate integration** (#270):
230
+ `Phronomy::Tool::ScopePolicy` is a callable that maps `(tool_class, scope, agent)`
231
+ to `:allow`, `:approve`, or `:reject`. The default policy (`ScopePolicy::DEFAULT`)
232
+ automatically routes tools declaring high-risk scopes (`:write`, `:admin`,
233
+ `:external_network`, `:filesystem`, `:process`, `:external_process`) through the
234
+ existing approval gate; tools with `scope :read_only` or no scope are allowed
235
+ unconditionally. Per-agent policy overrides are available via
236
+ `agent.scope_policy = my_policy`.
237
+ **Behaviour change**: tools with the above scopes that previously executed without
238
+ an approval handler will now be **rejected** unless an approval handler is
239
+ registered or the agent uses a custom permissive policy.
240
+
241
+ - **`PromptInjectionGuardrail`, `Tool::Base#redact_params`, and `#max_result_size`** (#271):
242
+ `Phronomy::Guardrail::PromptInjectionGuardrail` is a built-in `InputGuardrail`
243
+ subclass that detects prompt-injection patterns in user input.
244
+ `Tool::Base.redact_params(*names)` marks parameter names as sensitive; their
245
+ values are replaced with `"[REDACTED]"` in log and trace output.
246
+ `Tool::Base.max_result_size(n)` sets a per-tool character limit; results
247
+ exceeding the limit are truncated and a warning is logged. The global fallback is
248
+ `Phronomy.configure { |c| c.tool_result_max_size = n }` (default: no limit).
249
+
250
+ - **`execution_mode` DSL on `Tool::Base`** (#263):
251
+ `Tool::Base.execution_mode` accepts `:cooperative`, `:blocking_io` (default),
252
+ `:cpu_bound`, or `:external_process`. Tools marked `:blocking_io` (the default)
253
+ are dispatched through `BlockingAdapterPool` when a `Runtime` is available,
254
+ keeping the scheduler thread unblocked. Tools marked `:cooperative` are called
255
+ directly on the scheduler thread (suitable for pure in-memory operations).
256
+
257
+ - **`invoke_async` and `call_async` — async entry points** (#262):
258
+ `Agent::Base#invoke_async(input, **opts)` returns a `Phronomy::Task` wrapping
259
+ `#invoke`. `Workflow#invoke_async(input, config:)` does the same for workflows.
260
+ `Tool::Base#call_async(args, cancellation_token:)` returns a `Task` wrapping
261
+ `#call`. All three are backward-compatible with existing synchronous callers.
262
+
263
+ - **`LLMAdapter` abstraction** (#266):
264
+ `Phronomy::LLMAdapter::Base` decouples the agent pipeline from RubyLLM.
265
+ `Phronomy::LLMAdapter::RubyLLM` (registered by default) wraps the existing
266
+ integration. Custom adapters can be registered via
267
+ `Phronomy.configure { |c| c.llm_adapter = MyAdapter }` for testing or
268
+ alternative LLM backends.
269
+
270
+ - **`BlockingAdapterPool` backpressure limits** (#268):
271
+ `BlockingAdapterPool` now enforces configurable `pool_size` (default: 10) and
272
+ `queue_size` (default: 100) limits. Tasks submitted when the queue is full raise
273
+ `Phronomy::BackpressureError` immediately instead of growing the queue without
274
+ bound.
275
+
276
+ - **Cooperative scheduler fairness** (#269):
277
+ The scheduler measures per-task lag and emits starvation and dispatch warnings
278
+ via `Phronomy.configuration.logger` when tasks wait longer than configured
279
+ thresholds. Configurable via `scheduler_starvation_warn_ms` and
280
+ `scheduler_dispatch_warn_ms`.
281
+
282
+ - **Workflow entry actions awaitable with Task** (#264):
283
+ Entry action lambdas may now return a `Phronomy::Task`. The FSMSession awaits
284
+ the task on a background thread and posts `:action_completed` (with the resulting
285
+ `WorkflowContext`) or `:state_completed` back to the EventLoop without blocking
286
+ it. Backward-compatible: lambdas that return a `WorkflowContext` or `nil`
287
+ continue to work as before.
288
+
289
+ - **`Task`, `TaskGroup`, `AsyncQueue`, `Deadline`, `InvocationContext`, `Runtime` concurrency abstractions** (#255):
290
+ Six new concurrency primitives form the foundation of the async execution layer.
291
+ `Task` wraps a callable with cancellation, timeout (`Deadline`), and context
292
+ propagation (`InvocationContext`). `TaskGroup` runs tasks concurrently and waits
293
+ for all to finish (or the first failure). `AsyncQueue` is a bounded, cancellable
294
+ queue. `Runtime` is the top-level façade that resolves a `BlockingAdapterPool`
295
+ and provides `blocking_io { }` and `cpu_bound { }` dispatch helpers.
296
+
297
+ - **`BlockingAdapterPool`** (#256):
298
+ A bounded thread pool that isolates blocking I/O (LLM calls, database queries,
299
+ HTTP requests) from the cooperative scheduler thread. Default pool size is 10
300
+ threads with a queue depth of 100. Replaces direct `Thread.new` calls in core
301
+ agent and tool paths.
302
+
303
+ - **`VectorStore#size` — document count for all backends, contract coverage for RedisSearch and Pgvector** (#240):
304
+ `VectorStore::Base` gains `#size` as an abstract method; `InMemory`, `RedisSearch`,
305
+ and `Pgvector` all implement it. `RedisSearch#size` queries `FT.INFO num_docs`;
306
+ `Pgvector#size` delegates to `model_class.count`. The `a_vector_store` shared example
307
+ is applied to RedisSearch and Pgvector (nightly real-backend CI); unit specs add a
308
+ skip-guarded `it_behaves_like` reference and dedicated `#size` unit tests.
309
+ `empty_store` override hook added to the shared example for real-backend callers.
310
+
311
+ - **`force_kill: false` default in `dispatch_parallel`, `fan_out`, and `EventLoop#stop`** (#235):
312
+ Thread#kill is now opt-in. The default `force_kill: false` leaves timed-out workers
313
+ running and raises `TimeoutError` immediately, avoiding the risk of interrupted
314
+ `ensure` blocks or corrupted database transactions. Pass `force_kill: true` to
315
+ restore the previous behaviour (with a `logger.warn` to make it visible).
316
+ `EventLoop#stop` gains the same keyword and returns `:timeout` instead of
317
+ `:force_killed` when `force_kill: false` and the thread is still alive.
318
+
319
+ - **Public API compatibility snapshot spec** (#236):
320
+ `spec/phronomy/public_api_spec.rb` enumerates expected public methods for every
321
+ `Stable`-tagged constant. The spec runs as part of the default RSpec suite; any
322
+ accidental removal or rename of a listed method now fails CI immediately.
323
+
324
+ - **Nightly real-backend CI split into three independent job groups** (#238):
325
+ The nightly workflow (`nightly.yml`) now has three separately skippable jobs:
326
+ `real-backend-redis` (Redis Stack), `real-backend-pgvector` (PostgreSQL + pgvector),
327
+ and `real-backend-otel` (OpenTelemetry in-process SDK exporter). Each job runs only
328
+ the relevant spec with `--tag real_backend:<backend>`. The existing `redis_search_spec`
329
+ and `pgvector_spec` gain the `real_backend:` metadata tag. A new `otel_spec.rb`
330
+ verifies span emission, attribute attachment, and error recording via
331
+ `InMemorySpanExporter`.
332
+
333
+ - **`CancellationToken#raise_if_cancelled!` — convenience cancellation check** (#234):
334
+ New instance method that raises `Phronomy::CancellationError` when the token is
335
+ cancelled, or returns `nil` otherwise. Replaces the `if cancelled? then raise`
336
+ pattern inside tools, RAG loaders, and hooks.
337
+
338
+ - **Tool cooperative cancellation via `cancellation_token:` keyword** (#234):
339
+ `Tool::Base#call` now injects `Thread.current[:phronomy_cancellation_token]` as
340
+ `cancellation_token:` into `execute` when the method declares that keyword. Existing
341
+ tools without the keyword continue to work unchanged. Tool authors can opt in:
342
+ `def execute(query:, cancellation_token: nil)`.
343
+
344
+ - **`CancellationToken.timeout_after` — monotonic-clock deadline** (#225):
345
+ New `CancellationToken.timeout_after(seconds)` class method creates a token that
346
+ becomes cancelled after the specified number of seconds, measured with
347
+ `Process::CLOCK_MONOTONIC` (immune to NTP/DST drift). The existing `deadline:`
348
+ keyword for wall-clock deadlines remains supported for backward compatibility.
349
+
350
+ - **`EventLoop#stop` — drain mode and cooperative shutdown** (#233):
351
+ `EventLoop#stop` now accepts a `drain: true` keyword (default: `false`). When
352
+ set, the loop waits up to `Phronomy.configuration.event_loop_stop_grace_seconds`
353
+ (default: 5 s, configurable) for in-flight FSM sessions to complete before
354
+ joining threads. New sessions submitted while shutdown is pending are rejected
355
+ immediately with `Phronomy::CancellationError`. A new
356
+ `event_loop_stop_grace_seconds` configuration attribute is available on
357
+ `Phronomy::Configuration`.
358
+
359
+ - **`invoke_timeout` DSL and `Phronomy::TimeoutError`**: Agents can declare a per-invoke
360
+ timeout in seconds via `invoke_timeout N` in the class body. Exceeding the timeout raises
361
+ `Phronomy::TimeoutError` (a subclass of `Phronomy::Error`). The default remains unlimited.
362
+
363
+ - **`dispatch_parallel` / `fan_out` — per-call `timeout:` option** (#133): Both methods now
364
+ accept `timeout: nil` (default, unlimited) or a positive `Numeric` in seconds. Timed-out
365
+ tasks are treated the same as errors and follow the existing `on_error:` policy (`:raise`
366
+ or `:skip`).
367
+
368
+ - **MCP `HttpTransport` custom authentication headers** (#144): `Phronomy::Tools::Mcp::HttpTransport#initialize`
369
+ now accepts `headers: {}`. Arbitrary headers (e.g. `Authorization: Bearer …`) are injected
370
+ into every JSON-RPC request, enabling use of MCP servers that require bearer tokens or
371
+ API keys. Threading `headers:` through `Mcp.from_server` is tracked in issue #144 and
372
+ pending in PR #151.
373
+
374
+ - **`StdioTransport` — `env:`, `cwd:`, and `startup_timeout:` options** (#145):
375
+ Three new keyword arguments are now accepted when constructing a `StdioTransport` (and
376
+ therefore via `McpTool.from_server`): `env: {}` merges extra variables into the child
377
+ process environment; `cwd: nil` sets the working directory; `startup_timeout: 5` limits
378
+ how long to wait for the child process to become ready.
379
+
380
+ - **Workflow DSL validates graph structure at build time** (#124): `Phronomy::Workflow.define`
381
+ now raises `ArgumentError` immediately for hard structural errors (no states declared,
382
+ transitions referencing undefined targets). Unreachable states emit a warning but do not
383
+ raise. Errors surface at load time rather than at the first `invoke`.
384
+
385
+ - **Expanded error taxonomy** (#149): Five new subclasses of `Phronomy::Error` are now
386
+ available: `TransportError` (MCP or LLM network-layer failure; subclasses are
387
+ `RateLimitError` for HTTP 429 and `AuthenticationError` for HTTP 401/403),
388
+ `ContextLengthError` (prompt exceeds model context window), and
389
+ `CancellationError` (explicit invocation cancellation, distinct from the
390
+ deadline-exceeded `TimeoutError`). All five are defined as subclasses of
391
+ `Phronomy::Error` so application code can rescue them uniformly.
392
+
393
+ - **`Agent::Base.static_knowledge_refresh!`** (#164): New class-level method that clears the
394
+ cached `static_knowledge` chunks so the next `invoke` re-fetches from all registered
395
+ sources. Essential for long-running processes (web servers, job workers) where knowledge
396
+ sources may be updated at runtime without a process restart.
397
+
398
+ - **`Phronomy::Configuration#logger`** (#158): New optional configuration attribute. Any
399
+ object responding to `#warn` (e.g. `Rails.logger`) can be assigned. Framework diagnostic
400
+ messages — starting with the unreachable-state warning from `Workflow.define` — are routed
401
+ through this logger instead of writing directly to `$stderr` via `Kernel#warn`.
402
+
403
+ - **`Phronomy.with_configuration` and `Phronomy.reset_runtime!`** (#206): Two new class
404
+ methods for runtime isolation. `with_configuration` yields the current `Configuration`
405
+ object and restores the original after the block — even on exception — enabling per-request
406
+ overrides and scoped test configuration. `reset_runtime!` stops any running `EventLoop`,
407
+ clears its singleton, and resets configuration to defaults; intended for test suites to
408
+ ensure clean state between examples. `spec_helper.rb` now calls `reset_runtime!` in an
409
+ `after(:each)` hook automatically.
410
+
411
+ - **`CancellationToken` — cooperative cancellation for agent invocations** (#216):
412
+ New class `Phronomy::CancellationToken` enables cooperative cancellation without
413
+ `Thread#kill`. Tokens are passed via `config: { cancellation_token: token }`.
414
+ `cancel!` marks the token (thread-safe via Mutex); `cancelled?` returns `true`
415
+ once cancelled or once an optional `deadline: Time` has passed. Agents check the
416
+ token in `_invoke_impl` (fail-fast before any LLM call) and again immediately
417
+ before `chat.ask`. `CancellationError` is never retried by the retry policy.
418
+ `dispatch_parallel` and `fan_out` accept `cancellation_token:` and automatically
419
+ inject it into every worker task's config unless the task already supplies its own.
420
+
421
+ ### Added (post-v0.9.0)
422
+
423
+ - **`Agent::Base#run_as_child` removed** (post-v0.9.0):
424
+ `run_as_child` was introduced when agents had their own FSM. After `Agent::FSM`
425
+ was removed, the `:child_completed` event payload was silently discarded by
426
+ `FSMSession`, meaning `ctx.answer` was never populated. The method has been
427
+ removed. Use `invoke_async + Task#map` instead:
428
+
429
+ ```ruby
430
+ # Before (deprecated)
431
+ entry :translate, ->(ctx) { TranslationAgent.new.run_as_child(ctx.query, ctx: ctx) }
432
+ transition from: :translate, on: :child_completed, to: :done
433
+
434
+ # After (recommended)
435
+ entry :translate, ->(ctx) {
436
+ TranslationAgent.new.invoke_async(ctx.query).map { |r| ctx.merge(answer: r[:output]) }
437
+ }
438
+ transition from: :translate, to: :done
439
+ ```
440
+
441
+ - **`Phronomy::Agent::CheckpointStore` — idempotency store for HITL resume** (post-v0.9.0):
442
+ New in-memory store tracks consumed checkpoint IDs. Calling `Agent::Base#resume` twice
443
+ with the same checkpoint raises `Phronomy::CheckpointAlreadyResumedError` instead of
444
+ silently re-executing the approved tool. Custom stores can be injected via
445
+ `agent.checkpoint_store = MyRedis::CheckpointStore.new`. Duck-type contract:
446
+ `consumed?(id)`, `consume!(id)`, and optionally `cleanup!(id)` / `clear!`.
447
+
448
+ - **`checkpoint_id`, `agent_class`, `requested_at` on `Checkpoint`; `Agent::Base.resume` class method** (post-v0.9.0):
449
+ `Checkpoint` now carries a UUID `checkpoint_id` (idempotency key), `agent_class`
450
+ (fully-qualified class name), and `requested_at` (UTC timestamp). The new class-level
451
+ `Agent::Base.resume(checkpoint, approved:)` method instantiates the correct agent class
452
+ automatically and delegates to `#resume`, simplifying job-queue resume flows.
453
+
454
+ - **`CheckpointStore#cleanup!` and `#clear!`** (post-v0.9.0):
455
+ Optional methods on the `CheckpointStore` duck-type contract. `cleanup!(checkpoint_id)`
456
+ removes a single checkpoint entry; `clear!` wipes all tracking state.
457
+ ### Removed
458
+
459
+ - **`Phronomy::ReactAgent` class removed** (post-v0.9.0):
460
+ Use `Phronomy::Agent::Base` directly. `ReactAgent` had no distinct public API beyond
461
+ `Agent::Base` and was not listed in the stability table.
462
+
463
+ - **`Phronomy::Agent::FSM` class removed** (post-v0.9.0, internal):
464
+ The agent invocation path is now unified through `Agent::Base#invoke` with inline logic.
465
+ No public API impact.
466
+
467
+ - **`Phronomy::Agent::Lifecycle::FSMSession` and `::PhaseMachineBuilder` moved to `Workflow` namespace** (post-v0.9.0, internal):
468
+ These internal classes now live at `Phronomy::Workflow::FSMSession` and
469
+ `Phronomy::Workflow::PhaseMachineBuilder`. No public API impact.
470
+
471
+ - **BREAKING: `Agent::Base#run_as_child` drops `&result_writer` block parameter** (#265):
472
+ The optional block form `run_as_child(input, ctx: ctx) { |r| ctx.answer = r[:output] }`
473
+ is no longer supported. The result is now delivered **exclusively** as the
474
+ `:child_completed` event payload `{ output:, messages:, usage: }`. The parent
475
+ Workflow task is the sole owner of the `WorkflowContext`; no background thread
476
+ writes to it directly. Callers that were using the block to write back into the
477
+ context must update their workflow design (e.g. read the result in the target
478
+ state's entry action after the transition, or store output through an external
479
+ shared resource if needed).
480
+
481
+ - **BREAKING (internal): `AgentFSM#initialize` drops `result_writer:` keyword** (#265):
482
+ Direct callers of `AgentFSM.new(result_writer: ...)` must remove that keyword.
483
+ This class is considered internal; gem consumers should use `run_as_child` instead.
484
+
485
+ ### Changed
486
+
487
+ - **`AgentFSM`, `ParallelToolChat`, and `Orchestrator` use `Task`/`TaskGroup` instead of bare `Thread.new`** (#257, #258, #259):
488
+ All three components now spawn async work through the `Task` and `TaskGroup`
489
+ abstractions. This enables cancellation propagation, context threading, and
490
+ `BlockingAdapterPool` routing. No public API changes; behaviour is equivalent.
491
+
492
+ - **`Thread.current[:phronomy_*]` context propagation replaced with explicit `InvocationContext`** (#260):
493
+ Thread-local keys `phronomy_event_loop_thread`, `phronomy_cancellation_token`,
494
+ and `phronomy_context_version_caches` are no longer used as the primary
495
+ propagation channel. `InvocationContext` is threaded explicitly through call
496
+ stacks. Importantly, `Tool::Base#call` no longer falls back to
497
+ `Thread.current[:phronomy_cancellation_token]`; cancellation is only observed
498
+ when the caller passes `cancellation_token:` explicitly (or when
499
+ `ParallelToolChat` injects it). Tools that relied on the thread-local fallback
500
+ must be updated.
501
+
502
+ - **`Timeout.timeout` removed from core paths; replaced with `CancellationScope`** (#261):
503
+ `Agent::Base#invoke` and `McpTool::StdioTransport` no longer use `Timeout.timeout`
504
+ (which is unsafe with `Thread.new` and `ensure` blocks). A `CancellationScope`
505
+ with `deadline_in(seconds)` provides equivalent semantics without the thread-
506
+ interruption hazards. `ScopeTimeoutError < TimeoutError` is raised on expiry.
507
+
508
+ - **RAG/VectorStore blocking I/O placed behind `BlockingAdapterPool` async boundary** (#267):
509
+ `KnowledgeSource#fetch` and all three `VectorStore` backends now execute their
510
+ blocking I/O through `Runtime#blocking_io` when a `Runtime` is present. Callers
511
+ in a synchronous context see no change; callers in an EventLoop context benefit
512
+ from non-blocking scheduler behaviour.
513
+
514
+
515
+ The cancellation token (passed via `config: { cancellation_token: token }`) is
516
+ now checked at multiple additional points beyond the initial LLM call boundary:
517
+ before each `KnowledgeSource#fetch` in `build_context` (RAG phase); after each
518
+ streaming chunk in `_stream_impl`; before each tool-call batch in
519
+ `ParallelToolChat`; and after each `before_completion` hook. This ensures that
520
+ long-running retrieval, streaming, and tool-dispatch phases respect cancellation
521
+ with minimal latency.
522
+
523
+ - **`Agent::Orchestrator` uses `CancellationToken` for internal stop flag** (#224):
524
+ The boolean stop flag in `Orchestrator` is replaced with an internal
525
+ `CancellationToken`. FSM session loops perform cooperative cancellation checks
526
+ via `cancelled?`; `Thread#kill` is retained only as a last resort after
527
+ cooperative shutdown.
528
+
529
+ - **Error taxonomy classes are now raised at the retry boundary** (#204): The classes
530
+ `Phronomy::RateLimitError`, `Phronomy::AuthenticationError`, `Phronomy::ContextLengthError`,
531
+ and `Phronomy::TransportError` (introduced in #149) are now actually raised when the
532
+ corresponding `RubyLLM` exceptions occur. A new internal `ErrorTranslation` concern wraps
533
+ the retry exhaust path and maps `RubyLLM::*` exceptions to their Phronomy counterparts,
534
+ preserving the original exception as `#cause`. **Migration**: callers rescuing
535
+ `RubyLLM::RateLimitError` (or other `RubyLLM::*` errors) directly should migrate to
536
+ `rescue Phronomy::RateLimitError` / `Phronomy::TransportError` etc.
537
+
538
+ - **`Orchestrator#bounded_map` uses cooperative cancellation before force-kill** (#203):
539
+ Workers now check a shared `cancelled` flag at each loop iteration and stop picking up new
540
+ tasks once the timeout deadline passes. A 0.5 s grace period is given to in-flight workers
541
+ before `Thread#kill` is used as a last resort. `EventLoop#stop` similarly logs a warning
542
+ via `Phronomy.configuration.logger` when force-kill is triggered.
543
+
544
+ - **`Orchestrator#bounded_map` timeout deadline uses monotonic clock** (#209): Replaced
545
+ `Time.now` deadline arithmetic with `Process.clock_gettime(Process::CLOCK_MONOTONIC)` to
546
+ avoid sensitivity to NTP adjustments, DST transitions, and system-clock changes that could
547
+ inflate or deflate effective timeouts.
548
+
549
+ - **`EventLoop` warns on events for unknown `target_id`**: When the event loop receives an
550
+ event whose `target_id` does not match any registered session, a warning is emitted instead
551
+ of silently discarding the event.
552
+
553
+ - **`VectorStore#search` validates `k` is a positive integer**: All three backends
554
+ (`InMemory`, `RedisSearch`, `Pgvector`) now raise `ArgumentError` immediately when `k` is
555
+ not a positive integer, providing a clear error instead of a silent empty result or an
556
+ obscure database error.
557
+
558
+ - **`max_parallel_tools` DSL**: Agents can cap the number of concurrent tool-call threads
559
+ with `max_parallel_tools N` in the class body. Useful for rate-limiting external API calls.
560
+ The default is **10** (inheriting from `Base`); set explicitly to raise or lower the cap.
561
+
562
+ - **`max_parallel_tools` and `invoke_timeout` DSL argument validation** (#152): Both setters
563
+ now raise `ArgumentError` at class-definition time if the supplied value is invalid
564
+ (`max_parallel_tools` requires an `Integer >= 1`; `invoke_timeout` requires a positive
565
+ `Numeric`), surfacing configuration mistakes immediately.
566
+
567
+ - **`on_error :suppress` — canonical alias for `:return_empty`** (#165): `:suppress` is the
568
+ new preferred name for the error-suppression behaviour in `Tool::Base`. `:return_empty`
569
+ continues to function but emits a deprecation warning and will be removed in a future major
570
+ release. Migrate by replacing `on_error :return_empty` with `on_error :suppress`.
571
+
572
+ - **Tool nested object properties injected into JSON Schema** (#162): `Tool::Base#params_schema`
573
+ now recursively serialises nested `:object` param specs (including `enum` constraints and
574
+ further nesting) into the JSON Schema `properties` structure forwarded to the LLM,
575
+ enabling accurate structured argument generation for complex tool parameters.
576
+
577
+ ### Fixed
578
+
579
+ - **`tool_name` preserved in `Orchestrator#prepare_tool_class` anonymous subclass wrapper**:
580
+ When `Orchestrator#prepare_tool_class` wrapped a subagent tool in an anonymous
581
+ subclass (`Class.new(prepared)`), the class-level instance variable `@tool_name`
582
+ was not inherited, causing the wrapper's `tool_name` to return `nil`. RubyLLM
583
+ then registered the tool under a `nil` key, making it unreachable when the LLM
584
+ called it by name. The fix captures the effective name before subclassing and
585
+ calls `tool_name effective_name` explicitly inside the anonymous class body —
586
+ the same pattern already used by the approval-gate wrapper.
587
+
588
+ - **`EventLoop#start` is now idempotent; stale `:__stop__` sentinel race fixed** (#203):
589
+ Calling `start` on an already-running `EventLoop` is now a no-op. Fixed a race condition
590
+ where `stop` setting `@running = false` before the worker thread was scheduled left the
591
+ `:__stop__` sentinel unconsumed in the queue; a subsequent `start` would then immediately
592
+ terminate the new thread upon popping the stale sentinel. The sentinel is now treated as a
593
+ pure unblock signal for `queue.pop` (`next` instead of `break`) — loop termination is
594
+ driven solely by `@running`.
595
+
596
+ - **`trace_pii: false` now redacts both input and output**: Previously only the user input
597
+ was redacted when `trace_pii` was `false`; LLM responses and tool results were still
598
+ forwarded to the tracing backend unredacted. Both sides are now replaced with `[REDACTED]`.
599
+
600
+ - **`StdioTransport` — `read_timeout` prevents indefinite blocking**: A configurable
601
+ `read_timeout` (default 30 s) is now enforced on MCP stdio reads. A silent child process
602
+ could previously block the calling thread forever.
603
+
604
+ - **MCP schema `required` and `enum` constraints propagated to `param` DSL**:
605
+ `McpTool.from_server` now copies `required` and `enum` constraints from the MCP JSON Schema
606
+ into the generated `param` declarations so downstream validation sees them.
607
+
608
+ - **`FSMSession` notifies parent when child `AgentFSM` fails**: An unhandled error in a child
609
+ `AgentFSM` now correctly notifies the parent `FSMSession`, preventing it from waiting
610
+ indefinitely for a completion event that will never arrive.
611
+
612
+ - **`WorkflowContext.field` rejects plain `Array` or `Hash` defaults**: Passing a plain `Array`
613
+ or `Hash` as a field default now raises `ArgumentError` at class-definition time,
614
+ preventing accidental state sharing across workflow invocations. Other mutable objects
615
+ are not checked. Wrap collection defaults in a Proc: `default: -> { [] }`.
616
+
617
+ - **Tool aliases inherited by `Agent` subclasses**: `tool_aliases` declared in a parent
618
+ `Agent::Base` subclass are now correctly merged into subclasses rather than being silently
619
+ dropped.
620
+
621
+ - **`ReactAgent` output selection skips tool-role messages**: The final output selection
622
+ logic no longer misidentifies `tool`-role messages as the assistant response, fixing
623
+ spurious tool-call JSON appearing in `result[:output]`.
624
+
625
+ - **Thread-local context cache cleaned up after each `invoke`** (#128): `Agent::Base#invoke`
626
+ previously leaked thread-local context cache entries after each call, causing stale cache
627
+ hits in long-lived threads. The cache is now cleared in an `ensure` block.
628
+
629
+ - **Unknown tool parameters are rejected** (#130): `Tool::Base#call` now raises
630
+ `ArgumentError` when keyword arguments not declared via the `param` DSL are passed, instead
631
+ of forwarding them silently to `execute`.
632
+
633
+ - **`EventLoop#stop` uses cooperative shutdown instead of `Thread#kill`** (#135):
634
+ `Thread#kill` bypasses `ensure` blocks and is unsafe. The event loop now sets a sentinel
635
+ flag and joins the worker thread, allowing it to flush pending events before termination.
636
+
637
+ - **`Orchestrator` propagates parent `config` and `thread_id` to sub-agents** (#132):
638
+ Sub-agents spawned via `dispatch` or `dispatch_parallel` now inherit the caller's `config`
639
+ hash and `thread_id`, enabling correct memory isolation and distributed tracing in
640
+ multi-agent pipelines.
641
+
642
+ - **`Agent::Base` caches `static_knowledge` fetch at the class level** (#127): The RAG
643
+ knowledge fetch was re-executed on every `invoke`. The result is now memoized at the class
644
+ level (`@static_knowledge_chunks ||= ...`), eliminating redundant vector-store queries.
645
+ The cache is **not** invalidated automatically when source content changes; call
646
+ `static_knowledge_refresh!` explicitly to force a reload.
647
+
648
+ - **`WorkflowContext#initialize` raises on unknown field keys** (#121): Passing an
649
+ unrecognised key to `WorkflowContext.new` was silently ignored. The constructor now raises
650
+ `ArgumentError`, surfacing typos and API mismatches immediately.
651
+
652
+ - **`WorkflowContext#merge` raises `ArgumentError` for unknown field keys** (#154): Passing
653
+ an unrecognised key to `WorkflowContext#merge` was silently ignored. The method now raises
654
+ `ArgumentError`, matching the guard added to `#initialize` in #121.
655
+
656
+ - **`WorkflowContext#deep_dup_value` rescues `TypeError` for non-dupable objects** (#156):
657
+ Objects that raise `TypeError` from `#dup` (e.g. `Method`, frozen `Proc`, `Integer`,
658
+ `Symbol`) are now returned as-is instead of crashing.
659
+
660
+ - **`Workflow.define` raises for undefined `from:` state in transitions** (#157): Transitions
661
+ that reference a `from:` state not declared in the DSL now raise `ArgumentError` at
662
+ build time, complementing the existing check for undefined `to:` targets.
663
+
664
+ - **`Workflow.define` unreachable-state warning routes through configured logger** (#158):
665
+ The diagnostic warning for unreachable states now uses `Phronomy.configuration.logger`
666
+ when set, falling back to `Kernel#warn`. Previously the warning always went to `$stderr`.
667
+
668
+ - **`require "set"` added to `workflow.rb`** (#159): Eliminates an implicit dependency on
669
+ `Set` being pre-loaded by another gem.
670
+
671
+ - **`Tool::Base#validate_nested_object` rejects undeclared extra keys** (#166): Keys present
672
+ in the LLM-supplied hash but absent from the tool's nested `param` schema now produce a
673
+ validation error rather than being silently forwarded.
674
+
675
+ - **`WorkflowContext#merge` deep-copies unchanged fields** (#123): Fields absent from the
676
+ `merge` argument were previously shared by reference with the original context, allowing
677
+ one branch to mutate another branch's state. All fields are now independently copied.
678
+
679
+ - **Robust metadata parsing in `VectorStore::Pgvector#search`** (#139): Metadata stored as a
680
+ PostgreSQL JSON string is now parsed correctly regardless of whether the database driver
681
+ returns a `String` or an already-decoded `Hash`.
682
+
683
+ - **`OutputParser::JsonParser` tries all fenced code blocks before falling back** (#146):
684
+ The parser now scans every fenced block in the LLM response (in order) and returns the
685
+ first one that parses as valid JSON, rather than only checking the first block. This
686
+ improves reliability with models that include prose before the JSON block.
687
+
688
+ - **`on_error: :return_empty` emits a warning and returns a descriptive string** (#147):
689
+ Errors in tools that declare `on_error :return_empty` are now logged to `warn` before the
690
+ tool returns. The placeholder string includes the tool name and a brief reason, making
691
+ silent failures easier to diagnose.
692
+
693
+ - **`context_version_cache` accessible after `invoke` completes**: The thread-local cache is
694
+ cleared in `invoke`'s `ensure` block, which caused `context_version_cache` to return `nil`
695
+ immediately after every call. The value is now persisted in `@last_context_version_cache`
696
+ so it remains readable post-invoke.
697
+
698
+ - **`WorkflowContext` field type `:merge` comment corrected**: The inline comment incorrectly
699
+ described `:merge` as a deep-merge. It performs a shallow merge (`Hash#merge`). The comment
700
+ has been updated.
701
+
702
+ - **`WorkflowContext` return value from entry actions now adopted in EventLoop mode** (#107):
703
+ `FSMSession` previously discarded the `WorkflowContext` returned by entry action callables,
704
+ causing `s.merge(...)` updates to be silently lost when `event_loop = true`. The context is
705
+ now correctly propagated, bringing EventLoop semantics in line with the synchronous
706
+ `WorkflowRunner`. Regression tests added in `spec/phronomy/fsm_session_spec.rb` (unit)
707
+ and `spec/integration/workflow_spec.rb` (integration, both sync and EventLoop paths).
708
+
709
+ ### Documentation
710
+
711
+ - **`trace_pii = false` description corrected** (#153): The inline comment and README Note
712
+ now correctly state that both the input and the output are redacted.
713
+
714
+ - **`invoke_timeout` is a wait timeout, not cancellation** (#163): YARD comment now
715
+ explicitly documents that the background agent thread and in-flight LLM/tool calls are
716
+ **not** interrupted when the timeout fires. Only the caller receives `TimeoutError`.
717
+
718
+ - **`context_version_cache` thread-safety limitation documented** (#161): A NOTE in the YARD
719
+ comment explains that the per-instance cache is not thread-safe when the same agent
720
+ instance is shared across threads.
721
+
722
+ - **`trace_pii` option documented in README**: The `trace_pii:` configuration key and its
723
+ behaviour (default `false`, redacts input and output in trace records) is now described in
724
+ the Configuration section of the README.
725
+
726
+ - **CJK token under-count warning in `TokenEstimator`**: A note in both the source and README
727
+ explains that the byte-based heuristic under-counts CJK characters by roughly 3×. Users
728
+ processing Chinese, Japanese, or Korean content should apply a correction factor or use a
729
+ model-specific tokenizer.
730
+
731
+ - **Stability labels, `reset_configuration!` caveat, CI, and gemspec** (#140 / #141 / #142 / #143 / #148 / #150):
732
+ README stability table revised for several APIs. `Phronomy.reset_configuration!` now carries
733
+ a warning that it is intended for test isolation only. Gemspec upper bounds added for
734
+ `ruby_llm` and `pg`. `ruby head` added to the CI test matrix. README API smoke tests added.
735
+
736
+ ---
737
+
738
+ ## [0.6.0] - 2026-05-21
739
+
740
+ ### Removed
741
+
742
+ - **`Phronomy::Guardrail::Builtin` module removed**: `PromptInjectionDetector`
743
+ and `PIIPatternDetector` are opt-in pattern-matching helpers that encode
744
+ application-level policy decisions (which phrases to block, which PII
745
+ categories to detect, which languages to support). Shipping them as gem
746
+ defaults was misleading — their correct home is inside each application that
747
+ needs them. Reference implementations are now provided in example 06 of
748
+ `phronomy-examples`. Extend `Phronomy::Guardrail::InputGuardrail` directly to
749
+ create equivalent guardrails in your application.
750
+
751
+ ---
752
+
753
+ ## [0.5.4] - 2026-05-20
754
+
755
+ ### New Features
756
+
757
+ - **VectorStore embedding dimension validation** (#98): All three vector store
758
+ implementations (`InMemory`, `RedisSearch`, `Pgvector`) now validate that every
759
+ embedding passed to `add` and `search` matches the expected dimension.
760
+ Dimension is inferred automatically from the first `add` call; alternatively
761
+ it can be set explicitly via `initialize(dimension: N)`. A mismatch raises
762
+ `ArgumentError` with a descriptive message. The `search` method never
763
+ establishes the dimension — it only validates when a dimension is already
764
+ known. `clear` retains the established dimension (schema property).
765
+
766
+ - **`dispatch_parallel` / `fan_out` concurrency controls** (#99): Two new
767
+ keyword arguments are now accepted by both methods.
768
+ - `max_concurrency: nil` (default) or a positive `Integer` — caps the number
769
+ of worker threads. `nil` means one thread per task (previous behaviour).
770
+ - `on_error: :raise` (default) or `:skip` — controls failure handling.
771
+ `:raise` runs all tasks to completion then re-raises the first error in
772
+ input order (fail-last, not fail-fast). `:skip` fills failed slots with
773
+ `nil` and never raises.
774
+ The underlying implementation uses a `Queue`-based bounded worker pool
775
+ (`bounded_map`) for predictable resource usage.
776
+
777
+ ---
778
+
779
+ ## [0.5.3] - 2026-05-20
780
+
781
+ ### Bug Fixes
782
+
783
+ - **Ensure `from_server` closes transport on error** (#95): The short-lived
784
+ transport created inside `McpTool.from_server` is now wrapped in
785
+ `begin/ensure`, so the underlying child process (stdio) is always
786
+ terminated even when `fetch_tool` raises.
787
+ - **Correct `McpTool#close` documentation** (#94): The comment previously
788
+ stated that calling `execute` after `close` raises an error; in practice
789
+ the transport reopens automatically. The comment now reflects actual
790
+ behaviour.
791
+
792
+ ### Documentation
793
+
794
+ - **Fix CHANGELOG date for v0.5.1** (#96): The v0.5.1 entry had an
795
+ incorrect date of 2026-05-21; corrected to 2026-05-20.
796
+
797
+ ---
798
+
799
+ ## [0.5.2] - 2026-05-20
800
+
801
+ ### Bug Fixes
802
+
803
+ - **CHANGELOG correction for v0.5.1 MCP fix** (#90): The v0.5.1 entry
804
+ incorrectly stated that a `Mutex` was added to `StdioTransport#rpc_call`.
805
+ The actual fix was per-instance transport ownership (each `McpTool` instance
806
+ creates its own transport in `initialize`). Corrected the description.
807
+
808
+ ### Enhancements
809
+
810
+ - **Add `McpTool#close`** (#92): Tool instances now expose a `close` method
811
+ that shuts down the underlying stdio child process (`StdioTransport`) or
812
+ releases the HTTP connection (`HttpTransport`). This gives callers a
813
+ deterministic way to clean up resources instead of relying on GC.
814
+
815
+ ### Maintenance
816
+
817
+ - **Archive stale Rails integration design doc** (#91): Added an archived
818
+ notice to `spec/design/17_rails_integration.md` clarifying that Rails
819
+ integration was removed in v0.3.0–v0.5.1 and the document is for
820
+ historical reference only.
821
+
822
+ - **Remove zombie `register_workflow_context` API** (#93): The
823
+ `Phronomy.register_workflow_context`, `workflow_context_registry`, and
824
+ `reset_workflow_context_registry!` methods (along with the backing
825
+ `@workflow_context_registry` and `@registry_mutex` module-level variables)
826
+ were removed from `lib/phronomy.rb`. These existed to support the
827
+ `StateStore` deserialization guard, which was removed in a prior release.
828
+ The API had no remaining callers in the codebase and was not listed in
829
+ the README stability table.
830
+
831
+ ---
832
+
833
+ ## [0.5.1] - 2026-05-20
834
+
835
+ ### Bug Fixes
836
+
837
+ - **Remove broken Rails generator and Railtie** (#85): The generator template
838
+ referenced `Phronomy::ActiveRecord::ActsAs` which no longer exists, causing
839
+ `rails generate phronomy:install` to produce broken model files. Removed
840
+ `lib/generators/`, `lib/phronomy/railtie.rb`, and all references in
841
+ `lib/phronomy.rb`.
842
+
843
+ - **Fix MCP transport ownership** (#86): `McpTool` no longer stores a shared
844
+ transport at class level. `from_server` now uses a short-lived transport only
845
+ to fetch tool metadata and calls `close` immediately after. Each tool instance
846
+ creates its own `StdioTransport` or `HttpTransport` in `initialize`, so
847
+ concurrent callers (e.g. via `Orchestrator#dispatch_parallel`) never share
848
+ stdio streams. No `Mutex` is needed. Also adds missing
849
+ `require "securerandom"` and a no-op `HttpTransport#close` for interface
850
+ consistency.
851
+
852
+ ### Documentation
853
+
854
+ - **README corrections** (#87): Remove stale Rails generator installation
855
+ instructions. Clarify that `TeamCoordinator` worker state is local to a
856
+ single `invoke` call (not persistent across calls). Annotate app-level
857
+ examples (`09_rails_chat`, `15_rails_secure_chat`, `18_rails_agent_job`,
858
+ `19_trust_pipeline`) as requiring external infrastructure. Add scope note
859
+ to `Agent::Orchestrator` section.
860
+
861
+ ### Maintenance
862
+
863
+ - **Add version guard to `ruby_llm_patches.rb`** (#88): The monkey-patch for
864
+ the upstream `handle_error_chunk` bug (ruby_llm <= 1.15.0) is now
865
+ gated behind a `Gem::Version` check so upgrading ruby_llm will
866
+ automatically disable the override.
867
+
868
+ ---
869
+
870
+ ## [0.5.0] - 2026-05-20
871
+
872
+ ### Breaking Changes
873
+
874
+ - **`Agent::Base#invoke` and `#stream` — `messages` and `thread_id` promoted to
875
+ top-level keyword arguments**:
876
+ Previously these values were passed inside the `config:` hash. They are now
877
+ explicit keyword arguments. The `config:` hash retains other runtime options
878
+ such as `:knowledge_sources`, `:user_id`, and `:session_id`.
879
+
880
+ **Before (v0.4.x)**:
881
+ ```ruby
882
+ agent.invoke(input, config: { messages: prior_msgs, thread_id: "t1" })
883
+ agent.stream(input, config: { messages: prior_msgs, thread_id: "t1" }) { |e| ... }
884
+ ```
885
+ **After (v0.5.0)**:
886
+ ```ruby
887
+ agent.invoke(input, messages: prior_msgs, thread_id: "t1")
888
+ agent.stream(input, messages: prior_msgs, thread_id: "t1") { |e| ... }
889
+ ```
890
+ Applications that only pass `:knowledge_sources`, `:user_id`, or `:session_id`
891
+ in `config:` require no changes.
892
+
893
+ - **`Agent::Checkpoint#initialize` — `original_input:` is now a required keyword
894
+ argument**: Applications that construct `Checkpoint` instances directly must
895
+ add `original_input: input`. Checkpoints produced by `#invoke` already include
896
+ this field automatically.
897
+
898
+ ### Fixed
899
+
900
+ - **`ReactAgent#step` — system instructions were never applied**: The first
901
+ iteration of the ReAct loop now calls `build_context` to assemble the system
902
+ prompt and history, matching the behaviour of `Agent::Base`. Subsequent
903
+ iterations re-apply instructions via `build_cached_system_text` before calling
904
+ `chat.complete`. Previously, all iterations silently omitted the system prompt.
905
+
906
+ - **`Agent::Base#resume` — system instructions were not re-applied after
907
+ suspension**: Resuming from a `Checkpoint` now calls `build_cached_system_text`
908
+ using the original input stored in the checkpoint, so the LLM receives the
909
+ correct system prompt when the conversation continues. Previously, the LLM was
910
+ called without any system instructions on resume.
911
+
912
+ ---
913
+
914
+ ## [0.4.0] - 2026-05-19
915
+
916
+ ### Removed
917
+
918
+ - **`Phronomy::TrustPipeline` removed**: The `TrustPipeline` class and its inner
919
+ `TrustResult` value object have been deleted. Use `Phronomy::GeneratorVerifier`
920
+ instead, which provides the same generator-verifier pattern with a cleaner,
921
+ fully injectable API.
922
+
923
+ ### Added
924
+
925
+ - **`Phronomy::GeneratorVerifier`** — Generator-Verifier coordination loop
926
+ (Anthropic blog, Pattern 1). Wraps a generator agent and a verifier agent with
927
+ fully injectable prompt builders, response parsers, a configurable iteration
928
+ limit, and an approval-outcome raise policy.
929
+ - **`Phronomy::Agent::Orchestrator`** — Base class for orchestrator agents
930
+ (Anthropic blog, Pattern 2). Extends `Agent::Base` with a `subagent` DSL for
931
+ declarative subagent registration as LLM-callable tools, plus `dispatch_parallel`
932
+ and `fan_out` for programmatic parallel invocation.
933
+ - **`Phronomy::Agent::TeamCoordinator`** — Agent teams coordination pattern
934
+ (Anthropic blog, Pattern 3). An LLM-powered coordinator with a shared task
935
+ queue and a pool of worker agents that carry conversation history across task
936
+ assignments. Adds `coordinator_provider` DSL for independent LLM routing.
937
+ - **`Phronomy::Agent::SharedState`** — Shared-state coordination pattern
938
+ (Anthropic blog, Pattern 5). Peer agents collaborate via a `KnowledgeStore`;
939
+ the `member` DSL registers agents with per-agent instructions; `coordination`
940
+ sets the team protocol; `build_prompt` injects a tool-usage guide automatically.
941
+ - **`Phronomy::LowConfidenceError`** — Exception raised by `GeneratorVerifier`
942
+ when `raise_policy: :raise` and verification fails after exhausting the
943
+ iteration limit.
944
+
945
+ ### Changed
946
+
947
+ - **`Phronomy::Graph::StateGraph` event system refactored**: Per-node `advance`
948
+ events replaced with a unified `node_completed` event queue, reducing
949
+ event-handler registration overhead and simplifying listener registration.
950
+
951
+ ---
952
+
953
+ ## [0.3.0] - 2026-05-18
954
+
955
+ ### Removed
956
+
957
+ - **`Phronomy::Memory` module fully removed**: `ConversationManager`, all
958
+ `Storage` backends (InMemory, ActiveRecord), all `Retrieval` strategies
959
+ (Recent, Semantic, Composite), and all `Compression` helpers (ToolOutputPruner,
960
+ Summary) have been deleted. Conversation history is now the responsibility of
961
+ the calling application — pass prior messages via `config[:messages]`
962
+ (`Array<RubyLLM::Message>`) and receive the updated array in `result[:messages]`.
963
+ - **`Phronomy::StateStore` module fully removed**: `InMemory`, `ActiveRecord`,
964
+ `Redis`, and `FileSystem` state-store backends have been deleted. The Workflow
965
+ halted-state object is now returned directly from `invoke` and `send_event`
966
+ and must be stored by the caller if resumption is needed.
967
+ - **`Phronomy::Configuration#default_state_store` removed**: No longer meaningful
968
+ without a built-in state store.
969
+ - **`Phronomy::Configuration#default_memory` / `#memory_async` / `#memory_job_queue` removed**:
970
+ No longer meaningful without the Memory module.
971
+ - **Rails integration removed**: `Railtie` initializers for `AgentJob` and
972
+ `acts_as_phronomy_message` no longer load. The `rails/` and `active_record/`
973
+ directories have been deleted.
974
+ - **`Phronomy::Actor` and `Phronomy::ThreadActorRegistry` deleted**: The Active
975
+ Object pattern implementation (`actor.rb`, `thread_actor_registry.rb`) has been
976
+ removed. It provided synchronous blocking only (no true async) and was
977
+ architecturally inconsistent with the `WorkflowRunner` halt/resume model. All
978
+ thread coordination now uses plain `Mutex` where needed.
979
+ - **`Phronomy.configuration.max_actors` removed**: The configuration option is no
980
+ longer meaningful without `ThreadActorRegistry`.
981
+
982
+ ### Changed
983
+
984
+ - **`Agent::Base#invoke` and `#stream`** no longer route execution through a
985
+ per-thread Actor. Both methods now call `_invoke_impl` / `_stream_impl` directly
986
+ on the calling thread.
987
+ - **`Memory::Storage::InMemory`** now stores all thread data in an instance-level
988
+ `Hash` instead of `Thread.current` thread-local storage. The class-level
989
+ `THREAD_DATA_KEY` constant has been removed. `with_thread_lock` uses a
990
+ per-thread-id `Mutex` to preserve concurrent-compaction safety (issue #44).
991
+ - **`StateStore::InMemory`** now stores state in an instance-level `Hash`.
992
+ The `THREAD_DATA_KEY` constant has been removed.
993
+ - **`VectorStore::RedisSearch`** uses a `Mutex` for `ensure_index!` and `clear`
994
+ instead of an Actor, preserving the thread-safety invariant on `@index_created`.
995
+ - **`Tool::McpTool::StdioTransport`**, **`Tracing::LangfuseTracer`**,
996
+ **`TrustPipeline`**, and **`Memory::Retrieval::Semantic`** no longer hold a
997
+ dedicated Actor instance. All operations execute directly on the calling thread.
998
+ - **`PIIPatternDetector` — `:my_number` replaced by `:ssn`** ([#77]): The built-in PII
999
+ detector now checks for US Social Security Numbers (`\b\d{3}-\d{2}-\d{4}\b`) instead
1000
+ of Japanese My Numbers. The JIS X 0076 check-digit validation and `my_number_valid?`
1001
+ helper have been removed. Category key renamed from `:my_number` to `:ssn`.
1002
+ - **`PIIPatternDetector` — phone pattern updated to international format** ([#77]):
1003
+ The `:phone` pattern now matches 3-digit area code + 3–4-digit exchange + 4-digit
1004
+ subscriber number with optional E.164 country-code prefix
1005
+ (`(?:\+\d{1,3}[.\- ]?)?\(?\d{3}\)?[.\- ]?\d{3,4}[.\- ]?\d{4}\b`),
1006
+ replacing the previous Japan-specific pattern.
1007
+
1008
+ ### Fixed
1009
+
1010
+ - **`RubyLLM::Providers::OpenAI#handle_error_chunk` — `NoMethodError` on single-line SSE error chunks**:
1011
+ Some models (e.g. Qwen running via LM Studio) return SSE error events as a
1012
+ single line (`data: {...}`) without a preceding `event:` line. The upstream
1013
+ implementation called `chunk.split("\n")[1].delete_prefix(...)`, which raised
1014
+ `NoMethodError: undefined method 'delete_prefix' for nil` when the second
1015
+ element was absent. A monkey-patch in `lib/phronomy/ruby_llm_patches.rb` guards
1016
+ against this by returning an empty string when the split result has fewer than
1017
+ two elements.
1018
+ - **`README` — stale Memory API examples** ([#76]): All references to the
1019
+ non-existent `WindowMemory`, `ActiveRecordMemory`, `SemanticMemory` classes and
1020
+ `load_messages` / `memory_compression` API have been replaced with the correct
1021
+ `ConversationManager`-based API.
1022
+ - **`README` — `PIIPatternDetector` comment** ([#77]): Inline comment updated to
1023
+ `# Detect SSNs, credit cards, emails, and phone numbers`.
1024
+ - **`README` — Configuration block markdown** ([#80]): The `max_actors` Note block
1025
+ was incorrectly placed inside the Ruby code fence; moved outside so it renders
1026
+ as a blockquote.
1027
+ - **`README` — `Guardrails` stability label** ([#76]): Changed from `Stable` to `Beta`
1028
+ to reflect that the built-in detector patterns may evolve.
1029
+ - **`CHANGELOG` — stale entries** ([#78]): Removed the orphaned `[Unreleased]` section
1030
+ describing a never-released API, and replaced a forward `"As of 0.3.0"` reference
1031
+ with future-tense wording.
1032
+ - **`McpTool` — YARD class comment** ([#79]): Updated to document both the
1033
+ `stdio://` and `http://`/`https://` transport schemes.
1034
+ - **`README` — `max_actors` configuration reference** ([#80]): Added `c.max_actors`
1035
+ example and LRU eviction note to the Configuration section.
1036
+
1037
+ ---
1038
+
1039
+ ## [0.2.2] - 2026-05-17
1040
+
1041
+ ### Fixed
1042
+
1043
+ - **`Tool::Base` type validation — strict mode** ([#73]): Removed string-coercion
1044
+ pass-through for `:integer`, `:number`/`:float`, and `:boolean` parameters.
1045
+ A `String` value such as `"42"` now correctly raises a type error regardless
1046
+ of the `on_schema_error` mode. Fixes silent data corruption where the raw
1047
+ string was forwarded to `execute` instead of the expected numeric/boolean type.
1048
+ - **`README` — correct `send_event` API example** ([#69]): Fixed code sample that
1049
+ called `app.send_event(:approve, config: { thread_id: ... })` (positional args)
1050
+ which raises `ArgumentError` at runtime. Corrected to
1051
+ `app.send_event(state: state, event: :approve)`.
1052
+ - **`phronomy.gemspec` — exclude `vendor/` from gem package** (#70): `vendor/bundle`
1053
+ (~3 500 files, ~14 MB) was included in released gems. Added `"vendor/"` to the
1054
+ file reject list.
1055
+
1056
+ ### Added
1057
+
1058
+ - **`Phronomy::Configuration#max_actors`** ([#72]): New optional attribute
1059
+ (default `nil` = unlimited, backward-compatible). When set, `ThreadActorRegistry`
1060
+ enforces an LRU eviction policy: the least-recently-used actor is stopped and
1061
+ removed before a new one is created, preventing unbounded thread growth in
1062
+ long-running processes.
1063
+ - **`README` — feature stability table** ([#71]): Features section now uses a
1064
+ table with Stable / Beta / Experimental labels so users can assess maturity at
1065
+ a glance.
1066
+ - **`TrustPipeline::Result#citations` — unverified-source warning** ([#74]):
1067
+ YARD documentation now explicitly states that citations are extracted from the
1068
+ LLM's own output and have not been verified against any external source.
1069
+
1070
+ ### CI
1071
+
1072
+ - **Ruby 3.4 added to CI matrix** ([#75]): Aligns test coverage with the gemspec
1073
+ requirement (`>= 3.2.0`) and verifies compatibility with the current stable
1074
+ Ruby release.
1075
+
1076
+ [#69]: https://github.com/Raizo-TCS/phronomy/issues/69
1077
+ [#70]: https://github.com/Raizo-TCS/phronomy/issues/70
1078
+ [#71]: https://github.com/Raizo-TCS/phronomy/issues/71
1079
+ [#72]: https://github.com/Raizo-TCS/phronomy/issues/72
1080
+ [#73]: https://github.com/Raizo-TCS/phronomy/issues/73
1081
+ [#74]: https://github.com/Raizo-TCS/phronomy/issues/74
1082
+ [#75]: https://github.com/Raizo-TCS/phronomy/issues/75
1083
+
1084
+ ---
1085
+
1086
+ ## [0.2.1] — Unreleased
1087
+
1088
+ ### Changed
1089
+
1090
+ - **`WorkflowRunner` — state_machines fully drives execution** (architecture overhaul).
1091
+ Previously `state_machines` was used only for post-hoc transition validation;
1092
+ the next-node was calculated by Phronomy internally (`resolve_next_node`).
1093
+ After this change, all state transition decisions — including guard evaluation for
1094
+ routing events — will be delegated entirely to `state_machines`.
1095
+ - `PhaseTracker` now exposes `attr_accessor :context` so guard lambdas can
1096
+ access the `WorkflowContext` via `m.context`.
1097
+ - Guard bridge pattern: `if: ->(m) { guard_proc.call(m.context) }`.
1098
+ - Three event types registered per workflow:
1099
+ 1. `advance_<from>` — unconditional after-transitions
1100
+ 2. `<routing_event>` — guarded branching from action states (name is the
1101
+ event name used in the DSL, e.g. `:route`, `:route_review`)
1102
+ 3. `<external_event>` — human-in-the-loop triggers from wait states
1103
+ - Invalid transitions now raise `ArgumentError` instead of logging warnings.
1104
+ - **`WorkflowRunner` initializer signature changed** — `edges:`,
1105
+ `conditional_edges:`, and `wait_states:` replaced by `after_transitions:`,
1106
+ `route_transitions:`, `external_events:`, and `wait_state_names:`.
1107
+ This is an **internal-only** change; the public `Phronomy::Workflow.define` DSL
1108
+ is unchanged.
1109
+
1110
+ ### Removed (internal)
1111
+
1112
+ - `WorkflowRunner#resolve_next_node` — logic moved to state_machines
1113
+ - `WorkflowRunner#advance_phase` — replaced by `fire_event!`
1114
+ - `Workflow::Builder#build_edges`, `#build_conditional_edges`,
1115
+ `#build_wait_states` — replaced by unified event classification in `build`
1116
+
1117
+ ---
1118
+
1119
+ ## [0.2.0] - 2026-05-13
1120
+
1121
+ ### Added
1122
+
1123
+ - `Phronomy::Graph::WorkflowRunner` — state_machines-based execution engine
1124
+ (introduced as the internal successor to `CompiledGraph`).
1125
+ - `state.phase` — single source of truth for graph execution state (replaces
1126
+ `current_nodes` + `halted_before` dual attributes).
1127
+ - `state.halted?` — returns `true` when the graph is paused.
1128
+ - `CompiledGraph#add_wait_state` — declared a named wait state that halts
1129
+ automatically when reached (later superseded by `wait_state` DSL in `Workflow.define`).
1130
+ - `CompiledGraph#send_event(state:, event:, input: nil)` — event-driven resume API
1131
+ (later superseded by `app.send_event`).
1132
+
1133
+ ### Removed
1134
+
1135
+ - `ParallelNode` and `add_parallel_node` DSL. Use `Thread.new` or
1136
+ `Concurrent::Future` at the application level instead.
1137
+ - `Phronomy::Graph::TimeoutError` (was only used by `ParallelNode`).