phronomy 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -1
- data/README.md +11 -12
- data/lib/phronomy/agent/base.rb +0 -50
- data/lib/phronomy/task/mapped_backend.rb +78 -0
- data/lib/phronomy/task.rb +53 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8bcaf54fe256791053b04c22df3b58595d267b2c0ee16b67369fe2ecb36f19d
|
|
4
|
+
data.tar.gz: 9be01ec03a79ec5d557a3c51fc442992cd012a07e682a43cfa4c9e0358110fc8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44de75b5cc59ddf380aeb0be3abcf2a2c566cb8a1db6c35540525b86c74e7e0c06e75a3581e30276a25eea7f5f0334226e05b54d7d9694ed8f2b881d04a54e60
|
|
7
|
+
data.tar.gz: 76821f0cad1a918b762bb1d5737f902cc67b726f8dafb0dcf646b5cfc8dd4173527733a8a04e92e71ebea9a152fdb6ebaea20d9ccde3037f32d7fc32f5b05497
|
data/CHANGELOG.md
CHANGED
|
@@ -11,10 +11,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
## [0.
|
|
14
|
+
## [0.10.0] - 2026-06-08
|
|
15
15
|
|
|
16
16
|
### Added
|
|
17
17
|
|
|
18
|
+
- **`Task#map` — transform a Task's completed value** (#384):
|
|
19
|
+
`task.map { |result| ctx.merge(answer: result[:output]) }` returns a new `Task`
|
|
20
|
+
whose completed value is the block's return value. Primary use-case: wire
|
|
21
|
+
`Agent::Base#invoke_async` into a Workflow entry action so the agent result
|
|
22
|
+
reaches `WorkflowContext` through the standard `:action_completed` path without
|
|
23
|
+
requiring any changes to `FSMSession`. If the source task fails or is cancelled,
|
|
24
|
+
the mapped task propagates the error without calling the block.
|
|
25
|
+
|
|
26
|
+
### Removed
|
|
27
|
+
|
|
28
|
+
- **`Agent::Base#run_as_child` removed** (#384):
|
|
29
|
+
Introduced when agents had their own FSM (`Agent::FSM`). After `Agent::FSM` was
|
|
30
|
+
removed in v0.9.0, the `:child_completed` event payload was silently discarded by
|
|
31
|
+
`FSMSession`, so `ctx.answer` was never populated. Migrate to
|
|
32
|
+
`invoke_async + Task#map`:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# Before (removed)
|
|
36
|
+
entry :translate, ->(ctx) { TranslationAgent.new.run_as_child(ctx.query, ctx: ctx) }
|
|
37
|
+
transition from: :translate, on: :child_completed, to: :done
|
|
38
|
+
|
|
39
|
+
# After (recommended)
|
|
40
|
+
entry :translate, ->(ctx) {
|
|
41
|
+
TranslationAgent.new.invoke_async(ctx.query).map { |r| ctx.merge(answer: r[:output]) }
|
|
42
|
+
}
|
|
43
|
+
transition from: :translate, to: :done
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- **`Task#map` — transform a Task's completed value** (post-v0.9.0):
|
|
49
|
+
`task.map { |result| ctx.merge(answer: result[:output]) }` returns a new `Task`
|
|
50
|
+
whose completed value is the block's return value. The primary use-case is
|
|
51
|
+
connecting `Agent::Base#invoke_async` to a Workflow entry action so the agent
|
|
52
|
+
result populates the `WorkflowContext` through the standard `:action_completed`
|
|
53
|
+
path. If the source task fails or is cancelled, the mapped task propagates the
|
|
54
|
+
error without calling the block.
|
|
55
|
+
|
|
18
56
|
- **`Phronomy::Diagnostics` and `SchedulerReentrancyError`** (#278, #279):
|
|
19
57
|
`Phronomy::Diagnostics` exposes a snapshot of current scheduler state
|
|
20
58
|
(`pending_count`, `active_tasks`, `pool_utilization`, etc.) for debugging and
|
|
@@ -233,6 +271,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
233
271
|
|
|
234
272
|
### Added (post-v0.9.0)
|
|
235
273
|
|
|
274
|
+
- **`Agent::Base#run_as_child` removed** (post-v0.9.0):
|
|
275
|
+
`run_as_child` was introduced when agents had their own FSM. After `Agent::FSM`
|
|
276
|
+
was removed, the `:child_completed` event payload was silently discarded by
|
|
277
|
+
`FSMSession`, meaning `ctx.answer` was never populated. The method has been
|
|
278
|
+
removed. Use `invoke_async + Task#map` instead:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
# Before (deprecated)
|
|
282
|
+
entry :translate, ->(ctx) { TranslationAgent.new.run_as_child(ctx.query, ctx: ctx) }
|
|
283
|
+
transition from: :translate, on: :child_completed, to: :done
|
|
284
|
+
|
|
285
|
+
# After (recommended)
|
|
286
|
+
entry :translate, ->(ctx) {
|
|
287
|
+
TranslationAgent.new.invoke_async(ctx.query).map { |r| ctx.merge(answer: r[:output]) }
|
|
288
|
+
}
|
|
289
|
+
transition from: :translate, to: :done
|
|
290
|
+
```
|
|
291
|
+
|
|
236
292
|
- **`Phronomy::Agent::CheckpointStore` — idempotency store for HITL resume** (post-v0.9.0):
|
|
237
293
|
New in-memory store tracks consumed checkpoint IDs. Calling `Agent::Base#resume` twice
|
|
238
294
|
with the same checkpoint raises `Phronomy::CheckpointAlreadyResumedError` instead of
|
data/README.md
CHANGED
|
@@ -54,8 +54,9 @@ It provides composable building blocks — Workflows, Agents, Tools, Guardrails,
|
|
|
54
54
|
| Feature | Stability |
|
|
55
55
|
|---|---|
|
|
56
56
|
| **Workflow EventLoop Mode** — Opt-in event-driven execution: `Phronomy.configure { \|c\| c.event_loop = true }` | Experimental |
|
|
57
|
-
| **Agent EventLoop Mode** — `Agent#invoke` (non-blocking via EventLoop), `Agent#
|
|
57
|
+
| **Agent EventLoop Mode** — `Agent#invoke` (non-blocking via EventLoop), `Agent#invoke_async` + `Task#map` (child-agent pattern for Workflow integration), parallel tool dispatch via `ParallelToolChat` | Experimental |
|
|
58
58
|
| **`invoke_async` / `call_async`** — `Agent::Base#invoke_async` and `Workflow#invoke_async` return a `Task`; `Agent::Context::Capability::Base#call_async` similarly; compatible with EventLoop and standalone contexts | Experimental |
|
|
59
|
+
| **`Task#map`** — transforms a `Task`'s completed value via a block; returns a new `Task` whose value is the block's return value; if the source task fails or is cancelled the mapped task propagates the error without calling the block; primary use-case: `invoke_async.map { \|r\| ctx.merge(answer: r[:output]) }` to wire agent results into a `WorkflowContext` | Experimental |
|
|
59
60
|
| **CancellationToken** — Cooperative cancellation via `cancel!`/`cancelled?`/`raise_if_cancelled!`; `timeout_after(seconds)` for monotonic-clock deadlines; optional `deadline:` (wall-clock) for backward compatibility; passed as `config: { cancellation_token: token }` to agents and `dispatch_parallel`; injected into `tool.execute` when the method declares a `cancellation_token:` keyword | Experimental |
|
|
60
61
|
| **`dispatch_parallel` / `fan_out` `force_kill:` option** — `force_kill: false` (default) leaves timed-out workers running and raises `TimeoutError` immediately; `force_kill: true` restores the old `Thread#kill` behaviour with a `logger.warn` | Beta |
|
|
61
62
|
| **`execution_mode` DSL on `Agent::Context::Capability::Base`** — Declares how a tool's `execute` should be dispatched: `:cooperative` (same scheduler thread), `:blocking_io` (default; offloaded to `BlockingAdapterPool`), `:cpu_bound`, `:external_process` | Experimental |
|
|
@@ -199,20 +200,18 @@ final = app.send_event(state: state, event: :approve)
|
|
|
199
200
|
puts "Approved: #{final.approved}" # => true
|
|
200
201
|
```
|
|
201
202
|
|
|
202
|
-
In EventLoop mode (`c.event_loop = true`), `
|
|
203
|
-
asynchronously
|
|
204
|
-
|
|
205
|
-
dispatched. Always declare both transitions to avoid a stuck workflow:
|
|
203
|
+
In EventLoop mode (`c.event_loop = true`), use `invoke_async + Task#map` to run an agent
|
|
204
|
+
asynchronously inside a Workflow entry action. The mapped Task returns a `WorkflowContext`,
|
|
205
|
+
which `FSMSession` picks up via the standard `:action_completed` path:
|
|
206
206
|
|
|
207
207
|
```ruby
|
|
208
|
-
# EventLoop mode: workflow that runs an agent
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
# EventLoop mode: workflow that runs an agent and captures the result.
|
|
209
|
+
entry :translate, ->(ctx) {
|
|
210
|
+
TranslationAgent.new.invoke_async(ctx.query).map do |result|
|
|
211
|
+
ctx.merge(answer: result[:output]) # returns WorkflowContext
|
|
212
|
+
end
|
|
213
213
|
}
|
|
214
|
-
transition from: :
|
|
215
|
-
transition from: :run_agent, on: :child_failed, to: :handle_error
|
|
214
|
+
transition from: :translate, to: :done # no on: needed
|
|
216
215
|
```
|
|
217
216
|
|
|
218
217
|
### Multi-Agent — Agent-as-Tool pattern
|
data/lib/phronomy/agent/base.rb
CHANGED
|
@@ -532,56 +532,6 @@ module Phronomy
|
|
|
532
532
|
end
|
|
533
533
|
end
|
|
534
534
|
|
|
535
|
-
# Registers this agent as a child {AgentFSM} inside the given Workflow context.
|
|
536
|
-
#
|
|
537
|
-
# Use this method from a Workflow entry action (running on the EventLoop thread)
|
|
538
|
-
# instead of {#invoke}, which would raise a deadlock error because +invoke+ blocks
|
|
539
|
-
# on a +Thread::Queue+ when EventLoop mode is active.
|
|
540
|
-
#
|
|
541
|
-
# The agent runs asynchronously in a background IO thread. When it finishes, the
|
|
542
|
-
# parent {FSMSession} receives a +:child_completed+ event whose payload is the
|
|
543
|
-
# result hash +{ output:, messages:, usage: }+. Declare an +on: :child_completed+
|
|
544
|
-
# transition in your Workflow to advance to the next state.
|
|
545
|
-
#
|
|
546
|
-
# The result is delivered exclusively as the +:child_completed+ event payload.
|
|
547
|
-
# The parent Workflow task is the sole owner of the parent +WorkflowContext+ and
|
|
548
|
-
# applies the result after receiving the event — no background thread writes to
|
|
549
|
-
# the parent context directly.
|
|
550
|
-
#
|
|
551
|
-
# @example
|
|
552
|
-
# entry :run_agent, ->(ctx) { MyAgent.new.run_as_child(ctx.query, ctx: ctx) }
|
|
553
|
-
# transition from: :run_agent, on: :child_completed, to: :process_result
|
|
554
|
-
#
|
|
555
|
-
# @param input [String, Hash] user input passed to the agent
|
|
556
|
-
# @param ctx [Object] a WorkflowContext that responds to +#thread_id+
|
|
557
|
-
# @param messages [Array] prior conversation history
|
|
558
|
-
# @param config [Hash] invocation config (forwarded to +_invoke_impl+)
|
|
559
|
-
# @return [nil] the caller must not wait on any return value;
|
|
560
|
-
# the result arrives as a +:child_completed+ event
|
|
561
|
-
# @raise [Phronomy::Error] when EventLoop mode is not enabled
|
|
562
|
-
# @api public
|
|
563
|
-
def run_as_child(input, ctx:, messages: [], config: {})
|
|
564
|
-
unless Phronomy.configuration.event_loop
|
|
565
|
-
raise Phronomy::Error,
|
|
566
|
-
"run_as_child requires EventLoop mode. " \
|
|
567
|
-
"Enable with: Phronomy.configure { |c| c.event_loop = true }"
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
parent_id = ctx.thread_id
|
|
571
|
-
thread_id = "#{parent_id}_agent_#{SecureRandom.uuid}"
|
|
572
|
-
Phronomy::Runtime.instance.spawn(name: "agent-child:#{thread_id}") do
|
|
573
|
-
result = _invoke_impl(input, messages: messages, thread_id: thread_id, config: config)
|
|
574
|
-
Phronomy::EventLoop.instance.post(
|
|
575
|
-
Phronomy::Event.new(type: :child_completed, target_id: parent_id, payload: result)
|
|
576
|
-
)
|
|
577
|
-
rescue => e
|
|
578
|
-
Phronomy::EventLoop.instance.post(
|
|
579
|
-
Phronomy::Event.new(type: :child_failed, target_id: parent_id, payload: e)
|
|
580
|
-
)
|
|
581
|
-
end
|
|
582
|
-
nil
|
|
583
|
-
end
|
|
584
|
-
|
|
585
535
|
# Streaming version of #invoke. Yields {Phronomy::Agent::StreamEvent} objects
|
|
586
536
|
# as they are produced by the underlying LLM.
|
|
587
537
|
#
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
class Task
|
|
7
|
+
# Backend for Tasks created by {Task#map}.
|
|
8
|
+
#
|
|
9
|
+
# A mapped task's lifecycle is driven entirely by the +on_complete+
|
|
10
|
+
# callback of its source task — it never spawns a thread of its own.
|
|
11
|
+
# +MappedBackend+ transitions the owning task to +:running+ immediately
|
|
12
|
+
# on initialization so that +FSMSession+ treats it as an in-progress
|
|
13
|
+
# async action. Completion (or failure) is triggered externally via
|
|
14
|
+
# {Task#transition!} from the +on_complete+ callback registered by
|
|
15
|
+
# {Task#map}.
|
|
16
|
+
#
|
|
17
|
+
# +await+ and +join+ block until {#unblock} is called, which {Task#map}
|
|
18
|
+
# arranges by registering a second +on_complete+ callback on the *mapped*
|
|
19
|
+
# task itself after the transform callback has been registered.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
class MappedBackend < Backend
|
|
23
|
+
def initialize(task:, &)
|
|
24
|
+
super
|
|
25
|
+
@done_queue = Queue.new
|
|
26
|
+
task.transition!(:running)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Unblocks +await+ / +join+. Called by {Task#map} after the mapped task
|
|
30
|
+
# reaches a terminal state.
|
|
31
|
+
# @api private
|
|
32
|
+
def unblock(value, error)
|
|
33
|
+
@done_queue.push([value, error])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Blocks until the mapped task reaches a terminal state.
|
|
37
|
+
# @return [Object] the mapped value
|
|
38
|
+
# @raise [Exception] if the source task or the map block raised an error
|
|
39
|
+
# @api private
|
|
40
|
+
def await
|
|
41
|
+
value, error = @done_queue.pop
|
|
42
|
+
raise error if error
|
|
43
|
+
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns +false+ — a mapped task has no independent thread to kill.
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
# @api private
|
|
50
|
+
def alive?
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# No-op — mapped tasks carry no independent thread to cancel.
|
|
55
|
+
# @return [self]
|
|
56
|
+
# @api private
|
|
57
|
+
def cancel!
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Blocks until the mapped task completes, with an optional timeout.
|
|
62
|
+
# @param limit [Numeric, nil]
|
|
63
|
+
# @return [Object, nil] +nil+ on timeout
|
|
64
|
+
# @api private
|
|
65
|
+
def join(limit = nil)
|
|
66
|
+
if limit.nil?
|
|
67
|
+
await
|
|
68
|
+
else
|
|
69
|
+
begin
|
|
70
|
+
Timeout.timeout(limit) { await }
|
|
71
|
+
rescue Timeout::Error
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/phronomy/task.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "task/backend"
|
|
|
4
4
|
require_relative "task/thread_backend"
|
|
5
5
|
require_relative "task/immediate_backend"
|
|
6
6
|
require_relative "task/fiber_backend"
|
|
7
|
+
require_relative "task/mapped_backend"
|
|
7
8
|
|
|
8
9
|
module Phronomy
|
|
9
10
|
# A single unit of concurrent work.
|
|
@@ -195,6 +196,58 @@ module Phronomy
|
|
|
195
196
|
self
|
|
196
197
|
end
|
|
197
198
|
|
|
199
|
+
# Returns a new {Task} whose completed value is the result of applying
|
|
200
|
+
# +block+ to this task's completed value.
|
|
201
|
+
#
|
|
202
|
+
# If this task fails or is cancelled, the mapped task also fails/is
|
|
203
|
+
# cancelled with the same error. The block is never called in error cases.
|
|
204
|
+
#
|
|
205
|
+
# The primary use-case is transforming an agent result into a
|
|
206
|
+
# {WorkflowContext} so that a Workflow entry action can return a Task
|
|
207
|
+
# whose value is picked up by {Workflow::FSMSession} via the existing
|
|
208
|
+
# +:action_completed+ path:
|
|
209
|
+
#
|
|
210
|
+
# @example Returning agent output into a Workflow state field
|
|
211
|
+
# entry :translate, ->(ctx) {
|
|
212
|
+
# TranslationAgent.new.invoke_async(ctx.query).map do |result|
|
|
213
|
+
# ctx.merge(answer: result[:output]) # returns WorkflowContext
|
|
214
|
+
# end
|
|
215
|
+
# }
|
|
216
|
+
#
|
|
217
|
+
# @yield [value] the completed value of this task
|
|
218
|
+
# @yieldreturn [Object] the value for the mapped task
|
|
219
|
+
# @return [Task] a new task that completes when this task does
|
|
220
|
+
# @api public
|
|
221
|
+
def map(&block)
|
|
222
|
+
# MappedBackend drives the task lifecycle entirely via on_complete;
|
|
223
|
+
# it never spawns a thread of its own.
|
|
224
|
+
mapped = self.class.spawn(
|
|
225
|
+
name: "#{@name}-mapped",
|
|
226
|
+
parent: @parent,
|
|
227
|
+
backend_class: MappedBackend
|
|
228
|
+
) {}
|
|
229
|
+
|
|
230
|
+
on_complete do |value, error|
|
|
231
|
+
mapped_value = nil
|
|
232
|
+
mapped_error = error
|
|
233
|
+
unless error
|
|
234
|
+
begin
|
|
235
|
+
mapped_value = block.call(value)
|
|
236
|
+
rescue => e
|
|
237
|
+
mapped_error = e
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
if mapped_error
|
|
241
|
+
mapped.transition!(:failed, error: mapped_error)
|
|
242
|
+
else
|
|
243
|
+
mapped.transition!(:completed, value: mapped_value)
|
|
244
|
+
end
|
|
245
|
+
# Unblock mapped.await / mapped.join after the terminal transition.
|
|
246
|
+
mapped.backend.unblock(mapped_value, mapped_error)
|
|
247
|
+
end
|
|
248
|
+
mapped
|
|
249
|
+
end
|
|
250
|
+
|
|
198
251
|
# Returns +true+ once the task has finished (success, error, or cancellation).
|
|
199
252
|
# @return [Boolean]
|
|
200
253
|
# @api private
|
data/lib/phronomy/version.rb
CHANGED
data/lib/phronomy.rb
CHANGED
|
@@ -123,7 +123,8 @@ module Phronomy
|
|
|
123
123
|
# Raised when a {Phronomy::WorkflowContext} field is mutated from a thread
|
|
124
124
|
# that does not own the context (i.e. not the EventLoop dispatch thread).
|
|
125
125
|
# Only raised in EventLoop mode. Use +context.merge(...)+ to produce a new
|
|
126
|
-
# context, or deliver updates as +:
|
|
126
|
+
# context, or deliver updates as +:action_completed+ event payloads
|
|
127
|
+
# via {Agent::Base#invoke_async} + {Task#map}.
|
|
127
128
|
class WorkflowContextOwnershipError < Error; end
|
|
128
129
|
|
|
129
130
|
class << self
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phronomy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raizo T.C.S
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -189,6 +189,7 @@ files:
|
|
|
189
189
|
- lib/phronomy/task/backend.rb
|
|
190
190
|
- lib/phronomy/task/fiber_backend.rb
|
|
191
191
|
- lib/phronomy/task/immediate_backend.rb
|
|
192
|
+
- lib/phronomy/task/mapped_backend.rb
|
|
192
193
|
- lib/phronomy/task/thread_backend.rb
|
|
193
194
|
- lib/phronomy/task_group.rb
|
|
194
195
|
- lib/phronomy/testing.rb
|