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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/README.md +266 -38
  4. data/benchmark/bench_agent_invoke.rb +2 -3
  5. data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
  6. data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
  7. data/docs/mcp-client.md +75 -0
  8. data/examples/workflows/agent_event_mapping.rb +104 -0
  9. data/examples/workflows/generic_task_event_mapping.rb +58 -0
  10. data/gemfiles/mcp_1_0.gemfile +9 -0
  11. data/lib/phronomy/agent/agent_invocation.rb +385 -0
  12. data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
  13. data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
  14. data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
  15. data/lib/phronomy/agent/async_event_api.rb +471 -0
  16. data/lib/phronomy/agent/base.rb +509 -420
  17. data/lib/phronomy/agent/context/capability/base.rb +57 -119
  18. data/lib/phronomy/agent/llm_operation_result.rb +23 -0
  19. data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
  20. data/lib/phronomy/agent/tool_approval_request.rb +121 -0
  21. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
  22. data/lib/phronomy/agent/tool_executor.rb +47 -69
  23. data/lib/phronomy/agent/tool_invocation.rb +634 -0
  24. data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
  25. data/lib/phronomy/agent.rb +21 -9
  26. data/lib/phronomy/configuration.rb +58 -53
  27. data/lib/phronomy/diagnostics.rb +1 -1
  28. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
  29. data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
  30. data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
  31. data/lib/phronomy/engine/event_loop.rb +507 -303
  32. data/lib/phronomy/engine/fsm_session.rb +181 -140
  33. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
  34. data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
  35. data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
  36. data/lib/phronomy/engine/runtime.rb +247 -57
  37. data/lib/phronomy/engine/task.rb +5 -10
  38. data/lib/phronomy/event.rb +8 -8
  39. data/lib/phronomy/generator_verifier.rb +253 -142
  40. data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
  41. data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
  42. data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
  43. data/lib/phronomy/invocation_context.rb +5 -19
  44. data/lib/phronomy/llm_adapter/base.rb +25 -34
  45. data/lib/phronomy/metrics.rb +6 -3
  46. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
  47. data/lib/phronomy/stream_callback_error.rb +35 -0
  48. data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
  49. data/lib/phronomy/tools/mcp.rb +410 -81
  50. data/lib/phronomy/version.rb +1 -1
  51. data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
  52. data/lib/phronomy/workflow.rb +122 -261
  53. data/lib/phronomy/workflow_context.rb +55 -104
  54. data/lib/phronomy/workflow_runner.rb +239 -291
  55. data/lib/phronomy.rb +30 -23
  56. data/scripts/check_readme_runnable.rb +4 -1
  57. metadata +63 -11
  58. data/lib/phronomy/agent/concerns/retryable.rb +0 -103
  59. data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
  60. data/lib/phronomy/agent/invocation_context.rb +0 -171
  61. data/lib/phronomy/agent/invocation_session.rb +0 -346
  62. data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
  63. data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
  64. 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 inside the on_tool_call hook registered by InvocationSession
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
- # @return [Object] the RubyLLM tool_call object (responds to #name, #arguments, #id)
16
- attr_reader :tool_call
8
+ attr_reader :tool_calls
17
9
 
18
- # @param tool_call [Object] the RubyLLM tool_call object
19
- # @api private
20
- def initialize(tool_call)
21
- super("Tool call intercepted: #{tool_call.name}")
22
- @tool_call = tool_call
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 tool execution routing based on {Tool::Base.execution_mode}.
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
- # Dispatches a single tool call asynchronously according to its
52
- # +execution_mode+ and returns an awaitable.
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
- # @param tool [Phronomy::Agent::Context::Capability::Base] the tool instance to invoke
55
- # @param args [Hash] argument hash to pass to {Tool::Base#call}
56
- # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
57
- # @param config [Hash] invocation config forwarded from the agent pipeline.
58
- # Recognised keys: +:tool_timeout+ (seconds; passed as the
59
- # +BlockingAdapterPool#submit+ timeout so that timed-out
60
- # operations are tracked as abandoned rather than silently dropped).
61
- # @param runtime [Phronomy::Runtime] runtime to use for spawning
62
- # (defaults to {Runtime.instance}; injectable for tests)
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
- msg = if mode == :cpu_bound
57
+ message = if mode == :cpu_bound
77
58
  "[Phronomy] Tool #{tool.class.name} declares execution_mode :cpu_bound, " \
78
- "which has no dedicated executor. " \
79
- "Falling back to blocking_io (BlockingAdapterPool). " \
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
- "which has no dedicated process manager. " \
84
- "Falling back to blocking_io (BlockingAdapterPool)."
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(msg)
67
+ Phronomy.configuration.logger.warn(message)
88
68
  else
89
- warn msg
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
- # Submit directly to pool no wrapping Task thread required.
107
- # Pass tool_timeout so the pool can track timed-out operations as
108
- # abandoned, consistent with how LLM calls use config[:llm_timeout].
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