phronomy 0.13.0 → 0.15.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 +155 -0
- data/README.md +266 -38
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/docs/mcp-client.md +75 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/gemfiles/mcp_1_0.gemfile +9 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +509 -420
- data/lib/phronomy/agent/context/capability/base.rb +57 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +58 -53
- data/lib/phronomy/diagnostics.rb +1 -1
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
- data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
- data/lib/phronomy/engine/event_loop.rb +507 -303
- data/lib/phronomy/engine/fsm_session.rb +181 -140
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
- data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
- data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
- data/lib/phronomy/engine/runtime.rb +247 -57
- data/lib/phronomy/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +6 -3
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
- data/lib/phronomy/tools/mcp.rb +410 -81
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +55 -104
- data/lib/phronomy/workflow_runner.rb +239 -291
- data/lib/phronomy.rb +30 -23
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +63 -11
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -346
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
- data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
- data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
|
@@ -2,24 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
# Raised
|
|
6
|
-
# to intercept every tool call before RubyLLM executes it.
|
|
7
|
-
#
|
|
8
|
-
# Catching this exception in calling_llm_action lets the Agent FSM
|
|
9
|
-
# route through :executing_tool (and possibly :awaiting_approval) rather
|
|
10
|
-
# than executing the tool inside RubyLLM's internal loop.
|
|
11
|
-
#
|
|
12
|
-
# This class is intentionally NOT part of the public API.
|
|
5
|
+
# Raised by the Agent-owned RubyLLM ToolCall interceptor before execution.
|
|
13
6
|
# @api private
|
|
14
7
|
class ToolCallIntercepted < StandardError
|
|
15
|
-
|
|
16
|
-
attr_reader :tool_call
|
|
8
|
+
attr_reader :tool_calls
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
super("Tool call intercepted: #{
|
|
22
|
-
|
|
10
|
+
def initialize(tool_calls)
|
|
11
|
+
@tool_calls = Array(tool_calls).freeze
|
|
12
|
+
names = @tool_calls.map(&:name).join(", ")
|
|
13
|
+
super("Tool call intercepted: #{names}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Convenience accessor for callers that only support one ToolCall.
|
|
17
|
+
def tool_call
|
|
18
|
+
@tool_calls.first
|
|
23
19
|
end
|
|
24
20
|
end
|
|
25
21
|
end
|
|
@@ -2,91 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
# Centralises
|
|
6
|
-
#
|
|
7
|
-
# This is the single place in the framework that decides *how* a tool call is
|
|
8
|
-
# dispatched:
|
|
9
|
-
#
|
|
10
|
-
# - +:cooperative+ — dispatched via +Runtime#spawn+ through the configured
|
|
11
|
-
# scheduler. Under the +:fiber+ backend this avoids an
|
|
12
|
-
# extra OS thread; under the +:thread+ backend it is
|
|
13
|
-
# backed by +ThreadScheduler+ (one thread per task).
|
|
14
|
-
# - +:blocking_io+ — submitted to +BlockingAdapterPool+ when the runtime
|
|
15
|
-
# provides a pool; falls back to +Runtime#spawn+ otherwise.
|
|
16
|
-
# - +:cpu_bound+ — emits a deprecation-style warning then falls back to
|
|
17
|
-
# +:blocking_io+ routing (no process pool available yet).
|
|
18
|
-
# - +:external_process+ — falls back to +:blocking_io+ routing (no process
|
|
19
|
-
# manager available yet).
|
|
20
|
-
#
|
|
21
|
-
# All paths return an object that responds to +#await+ (+Phronomy::Task+ or
|
|
22
|
-
# +BlockingAdapterPool::PendingOperation+), so callers can collect results
|
|
23
|
-
# uniformly.
|
|
24
|
-
#
|
|
25
|
-
# @note Non-goals
|
|
26
|
-
# ToolExecutor deliberately does NOT provide:
|
|
27
|
-
# - A CPU-bound process pool. CPU-intensive tool work must be handled at the
|
|
28
|
-
# application layer (e.g., fork, Sidekiq, separate OS processes). The
|
|
29
|
-
# framework will not add a +ProcessPoolExecutor+ equivalent.
|
|
30
|
-
# - An external process manager. Spawning or supervising subprocesses is
|
|
31
|
-
# out of scope for this module.
|
|
32
|
-
# - Additional core execution routes beyond scheduler-backed cooperative
|
|
33
|
-
# execution and BlockingAdapterPool-backed blocking I/O isolation.
|
|
34
|
-
# The +:cpu_bound+ and +:external_process+ modes are accepted for
|
|
35
|
-
# compatibility but both fall back to +:blocking_io+ routing with a
|
|
36
|
-
# one-time warning. If a genuinely new core execution route is needed,
|
|
37
|
-
# a new ADR is required.
|
|
38
|
-
# These non-goals follow from the cooperative-first, non-preemptive
|
|
39
|
-
# concurrency model (ADR-010): framework components must not assume the
|
|
40
|
-
# caller's concurrency model, and CPU/process management belongs to the
|
|
41
|
-
# application layer.
|
|
5
|
+
# Centralises Tool execution routing based on execution_mode.
|
|
42
6
|
#
|
|
7
|
+
# Tool-specific timeout and retry belong to the Tool implementation or its
|
|
8
|
+
# underlying client. This executor only chooses the Phronomy execution
|
|
9
|
+
# resource and propagates cooperative cancellation.
|
|
43
10
|
# @api private
|
|
44
11
|
module ToolExecutor
|
|
45
|
-
# Tracks tool classes that have already emitted an execution_mode warning so
|
|
46
|
-
# that the same warning is only logged once per process lifetime.
|
|
47
12
|
WARNED_MODES = Set.new
|
|
48
13
|
WARNED_MODES_MUTEX = Mutex.new
|
|
49
14
|
private_constant :WARNED_MODES, :WARNED_MODES_MUTEX
|
|
50
15
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
16
|
+
# Agent-owned execution boundary. Only a ToolInvocation that has consumed
|
|
17
|
+
# authorization may enter this method.
|
|
18
|
+
def self.call_invocation_async(
|
|
19
|
+
tool_invocation:,
|
|
20
|
+
cancellation_token: nil,
|
|
21
|
+
config: {},
|
|
22
|
+
runtime: Phronomy::Runtime.instance
|
|
23
|
+
)
|
|
24
|
+
unless tool_invocation.dispatchable?
|
|
25
|
+
raise Phronomy::ToolError,
|
|
26
|
+
"ToolInvocation #{tool_invocation.id} is not authorized for dispatch"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
call_async(
|
|
30
|
+
tool: tool_invocation.tool,
|
|
31
|
+
args: tool_invocation.arguments,
|
|
32
|
+
cancellation_token: cancellation_token,
|
|
33
|
+
config: config,
|
|
34
|
+
runtime: runtime
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Low-level Tool API used by direct Tool#call_async callers. Agent execution
|
|
39
|
+
# must use .call_invocation_async so authorization cannot be bypassed.
|
|
53
40
|
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# @return [#await] a {Phronomy::Task} or {BlockingAdapterPool::PendingOperation}
|
|
64
|
-
# @api private
|
|
65
|
-
def self.call_async(tool:, args:, cancellation_token: nil, config: {}, runtime: Phronomy::Runtime.instance)
|
|
41
|
+
# +config+ remains available for invocation metadata, but Phronomy does not
|
|
42
|
+
# interpret it as a Tool timeout or retry policy.
|
|
43
|
+
def self.call_async(
|
|
44
|
+
tool:,
|
|
45
|
+
args:,
|
|
46
|
+
cancellation_token: nil,
|
|
47
|
+
config: {},
|
|
48
|
+
runtime: Phronomy::Runtime.instance
|
|
49
|
+
)
|
|
66
50
|
ct = cancellation_token
|
|
67
51
|
mode = tool.class.execution_mode
|
|
68
52
|
|
|
69
|
-
# Warn and normalise unsupported modes to :blocking_io.
|
|
70
|
-
# Each (tool class, mode) pair emits the warning at most once per process
|
|
71
|
-
# lifetime to avoid log flooding in high-throughput scenarios.
|
|
72
53
|
if mode == :cpu_bound || mode == :external_process
|
|
73
54
|
warn_key = [tool.class.name, mode]
|
|
74
55
|
newly_warned = WARNED_MODES_MUTEX.synchronize { WARNED_MODES.add?(warn_key) }
|
|
75
56
|
if newly_warned
|
|
76
|
-
|
|
57
|
+
message = if mode == :cpu_bound
|
|
77
58
|
"[Phronomy] Tool #{tool.class.name} declares execution_mode :cpu_bound, " \
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"Use :blocking_io explicitly to suppress this warning."
|
|
59
|
+
"which has no dedicated executor. Falling back to blocking_io " \
|
|
60
|
+
"(BlockingAdapterPool). Use :blocking_io explicitly to suppress this warning."
|
|
81
61
|
else
|
|
82
62
|
"[Phronomy] Tool #{tool.class.name} declares execution_mode :external_process, " \
|
|
83
|
-
|
|
84
|
-
|
|
63
|
+
"which has no dedicated process manager. Falling back to blocking_io " \
|
|
64
|
+
"(BlockingAdapterPool)."
|
|
85
65
|
end
|
|
86
66
|
if Phronomy.configuration.logger
|
|
87
|
-
Phronomy.configuration.logger.warn(
|
|
67
|
+
Phronomy.configuration.logger.warn(message)
|
|
88
68
|
else
|
|
89
|
-
warn
|
|
69
|
+
warn message
|
|
90
70
|
end
|
|
91
71
|
end
|
|
92
72
|
mode = :blocking_io
|
|
@@ -103,11 +83,9 @@ module Phronomy
|
|
|
103
83
|
tool.call(args, cancellation_token: ct)
|
|
104
84
|
end
|
|
105
85
|
else
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
timeout = config[:tool_timeout]
|
|
110
|
-
pool.submit(cancellation_token: ct, timeout: timeout) { tool.call(args, cancellation_token: ct) }
|
|
86
|
+
pool.submit(cancellation_token: ct) do
|
|
87
|
+
tool.call(args, cancellation_token: ct)
|
|
88
|
+
end
|
|
111
89
|
end
|
|
112
90
|
end
|
|
113
91
|
end
|