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