phronomy 0.11.1 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a6262c12bc063819c0e1656699f47666b06b182747d6ce85843d2a74988c358
4
- data.tar.gz: 6529858071913e61a6a840fe03e36620322bfddd50e415b054314124f9b7fcd1
3
+ metadata.gz: 9b5c5fc2f42107629541d051e1330e90e48298ccf19192440e7be77a30b6ca68
4
+ data.tar.gz: 78e4440ec72afb39b7021810c8cdfc8de44f94f15d97442f91000eff50aa967c
5
5
  SHA512:
6
- metadata.gz: 6b1441aab8f7197c23bd84fea750388280ee00036e729f47de13b0113249261f72fb10f01b5e9a78fb2464fcb0ebac5fd07d91eef7d4b5e83fb973b463c0630e
7
- data.tar.gz: d9c0401177c9d59900543c7ff5bbae698401fa0dc5ca7e33272458f41eddff2fa61caa501a5735b273f59b44de96e1fc70b0c5a648bb159066f7facd18b5aa82
6
+ metadata.gz: 79c98ed427ae1c55d245f871ba9248279199496b27535dfaca2b94d89998bfa95583c8b87a7c192f00f5143c755f6234a4f513ec37c86172ea2c286579e9b72a
7
+ data.tar.gz: 30d8c27a6a0a0d003da85438ee903fc73af648079c83ab42618ce53d98570b9cfaa39144a71c49e8cce77d94b0f679bb82b87b484e06e654eb8298379e8dc093
@@ -1173,7 +1173,7 @@ module Phronomy
1173
1173
  effective_name = resolved.new.name
1174
1174
  rejected_class = Class.new(resolved) do
1175
1175
  tool_name effective_name
1176
- define_method(:call) do |_args|
1176
+ define_method(:call) do |_args, **_kwargs|
1177
1177
  "Tool execution denied: scope :#{scope} is not permitted."
1178
1178
  end
1179
1179
  end
@@ -1199,9 +1199,9 @@ module Phronomy
1199
1199
  effective_name = resolved.new.name
1200
1200
  resolved = Class.new(resolved) do
1201
1201
  tool_name effective_name
1202
- define_method(:call) do |args|
1202
+ define_method(:call) do |args, **kwargs|
1203
1203
  if handler.call(name, args)
1204
- super(args)
1204
+ super(args, **kwargs)
1205
1205
  else
1206
1206
  "Tool execution denied."
1207
1207
  end
@@ -1216,8 +1216,8 @@ module Phronomy
1216
1216
  effective_name4 = resolved.new.name
1217
1217
  Class.new(resolved) do
1218
1218
  tool_name effective_name4
1219
- define_method(:call) do |args|
1220
- result = super(args)
1219
+ define_method(:call) do |args, **kwargs|
1220
+ result = super(args, **kwargs)
1221
1221
  result_filters.inject(result) { |val, f| f.call(val, tool_name: name, args: args) }
1222
1222
  end
1223
1223
  end
@@ -298,27 +298,36 @@ module Phronomy
298
298
  end
299
299
  end
300
300
 
301
- # Execute the tool off the EventLoop thread via Runtime.instance.spawn.
302
- # Tool implementations (e.g. Orchestrator sub-agent dispatch) may call
303
- # agent.invoke_async().wait_result which would deadlock if run directly on the
304
- # EventLoop dispatch thread. Returning a Task causes
305
- # PhaseMachineBuilder#dispatch_task to await the result off the EventLoop
306
- # and post :action_completed back when done.
301
+ # Dispatch the tool off the EventLoop thread via ToolExecutor, which
302
+ # routes based on the tool's execution_mode class attribute:
303
+ # :blocking_io (default) BlockingAdapterPool (bounded thread pool)
304
+ # :cooperative → Runtime.instance.spawn (scheduler task)
305
+ # Wrap the awaitable in Task.deferred so FSMSession recognises it as
306
+ # an async action and sets async_pending = true.
307
307
  tc_id = tc.id
308
308
  tc_args = tc.arguments
309
309
  tc_name = tc.name
310
- Phronomy::Runtime.instance.spawn(name: "tool-exec:#{tc_name}") do
311
- result = tool_instance.call(tc_args)
312
- ctx.chat.add_message(
313
- role: :tool,
314
- content: result.to_s,
315
- tool_call_id: tc_id
316
- )
317
- ctx.pending_tool_call = nil
318
- ctx.tool_call_pending = false
319
- ctx.approval_required = false
320
- ctx
310
+ ct = ctx.config[:cancellation_token]
311
+ awaitable = tool_instance.call_async(tc_args, cancellation_token: ct)
312
+ result_task = Phronomy::Task.deferred(name: "tool-exec:#{tc_name}")
313
+ awaitable.on_complete do |result, error|
314
+ if error
315
+ result_task.backend.unblock(nil, error)
316
+ result_task.transition!(:failed, error: error)
317
+ else
318
+ ctx.chat.add_message(
319
+ role: :tool,
320
+ content: result.to_s,
321
+ tool_call_id: tc_id
322
+ )
323
+ ctx.pending_tool_call = nil
324
+ ctx.tool_call_pending = false
325
+ ctx.approval_required = false
326
+ result_task.backend.unblock(ctx, nil)
327
+ result_task.transition!(:completed, value: ctx)
328
+ end
321
329
  end
330
+ result_task
322
331
  end
323
332
  private_class_method :executing_tool_action
324
333
 
@@ -252,9 +252,9 @@ module Phronomy
252
252
  effective_name = prepared.new.name
253
253
  Class.new(prepared) do
254
254
  tool_name effective_name
255
- define_method(:call) do |args|
255
+ define_method(:call) do |args, **kwargs|
256
256
  self._orchestrator_context = orch.instance_variable_get(:@_orchestrator_context)
257
- super(args)
257
+ super(args, **kwargs)
258
258
  end
259
259
  end
260
260
  end
@@ -35,39 +35,43 @@ module Phronomy
35
35
  # - "stdio://<command>" — spawn a child process
36
36
  # - "http://<url>" / "https://<url>" — connect to an HTTP/SSE server
37
37
  # @param tool_name [String] the tool name as registered in the MCP server
38
+ # @param headers [Hash] additional HTTP request headers forwarded to every
39
+ # request (tool discovery and tool execution). Ignored for stdio transports.
40
+ # Typical use: <tt>headers: { "Authorization" => "Bearer #{ENV['API_KEY']}" }</tt>
38
41
  # @return [Mcp] a configured subclass instance ready for use with an Agent
39
42
  # @api public
40
- def from_server(server_uri, tool_name:)
43
+ def from_server(server_uri, tool_name:, headers: {})
41
44
  # Use a short-lived transport only to query the tool definition,
42
45
  # then close it. Each Mcp instance creates its own transport
43
46
  # so that concurrent callers never share IO streams.
44
- transport = build_transport(server_uri)
47
+ transport = build_transport(server_uri, headers: headers)
45
48
  begin
46
49
  tool_def = transport.fetch_tool(tool_name)
47
50
  ensure
48
51
  transport.close
49
52
  end
50
- build_tool_class(tool_name, server_uri, tool_def).new
53
+ build_tool_class(tool_name, server_uri, tool_def, headers: headers).new
51
54
  end
52
55
 
53
56
  private
54
57
 
55
- def build_transport(uri)
58
+ def build_transport(uri, headers: {})
56
59
  scheme, path = uri.split("://", 2)
57
60
  case scheme
58
61
  when "stdio"
59
62
  StdioTransport.new(path)
60
63
  when "http", "https"
61
- HttpTransport.new(uri)
64
+ HttpTransport.new(uri, headers: headers)
62
65
  else
63
66
  raise ArgumentError, "Unsupported MCP transport scheme: #{scheme.inspect}. Supported: 'stdio://', 'http://', 'https://'."
64
67
  end
65
68
  end
66
69
 
67
- def build_tool_class(tool_name, server_uri, tool_def)
70
+ def build_tool_class(tool_name, server_uri, tool_def, headers: {})
68
71
  klass = Class.new(Mcp)
69
72
  klass.tool_name(tool_name)
70
73
  klass.instance_variable_set(:@mcp_server_uri, server_uri)
74
+ klass.instance_variable_set(:@mcp_headers, headers)
71
75
 
72
76
  # Register description and params from the MCP tool definition.
73
77
  klass.description(tool_def[:description] || tool_name)
@@ -82,7 +86,8 @@ module Phronomy
82
86
  # never share IO streams, eliminating the need for synchronisation.
83
87
  klass.define_method(:initialize) do
84
88
  uri = self.class.instance_variable_get(:@mcp_server_uri)
85
- @mcp_transport = self.class.send(:build_transport, uri)
89
+ hdrs = self.class.instance_variable_get(:@mcp_headers) || {}
90
+ @mcp_transport = self.class.send(:build_transport, uri, headers: hdrs)
86
91
  end
87
92
 
88
93
  klass.define_method(:execute) do |**args|
@@ -249,8 +254,10 @@ module Phronomy
249
254
  raise Phronomy::ToolError,
250
255
  "MCP stdio server did not start within #{@startup_timeout} seconds"
251
256
  end
252
- line = @stdout.gets
253
- @stdout.ungetbyte(line) if line
257
+ # Do NOT call @stdout.gets here: gets() blocks until a newline arrives,
258
+ # which hangs indefinitely when the server emits no startup line or a
259
+ # partial line without '\n'. IO.select already confirmed the server is
260
+ # alive and responsive; the first rpc_call will consume actual output.
254
261
  end
255
262
  end
256
263
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.11.1"
4
+ VERSION = "0.12.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S