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