Class: PiAgent::Client
- Inherits:
-
Object
- Object
- PiAgent::Client
- Defined in:
- lib/pi_agent/client.rb
Overview
High-level client. Owns a Transport, correlates request/response by id, and fans notifications out to subscribers.
client = PiAgent::Client.new.start
client.subscribe { |msg| ... } # all server-pushed messages
future = client.request("get_commands") # request/response
future.value!(timeout: 5) # blocks for response
client.notify("set_thinking", level: "off") # fire-and-forget (no id)
client.close
By default the client spawns ‘pi –mode rpc` as a local subprocess. Pass `transport_factory:` — a callable `(on_message:, on_stderr:) -> transport` — to run pi somewhere else (e.g. inside a remote sandbox). See Transport for the transport contract.
Constant Summary collapse
- DEFAULT_BIN =
"pi"- DEFAULT_ARGS =
["--mode", "rpc"].freeze
Instance Attribute Summary collapse
-
#bin ⇒ Object
readonly
Returns the value of attribute bin.
Class Method Summary collapse
Instance Method Summary collapse
- #alive? ⇒ Boolean
- #close ⇒ Object
-
#initialize(bin: nil, args: DEFAULT_ARGS, env: {}, cwd: nil, extension_ui: nil, transport_factory: nil) ⇒ Client
constructor
A new instance of Client.
- #notify(type, params = {}) ⇒ Object
- #request(type, params = {}) ⇒ Object
- #start ⇒ Object
- #subscribe(&block) ⇒ Object
- #unsubscribe(handle) ⇒ Object
Constructor Details
#initialize(bin: nil, args: DEFAULT_ARGS, env: {}, cwd: nil, extension_ui: nil, transport_factory: nil) ⇒ Client
Returns a new instance of Client.
48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/pi_agent/client.rb', line 48 def initialize(bin: nil, args: DEFAULT_ARGS, env: {}, cwd: nil, extension_ui: nil, transport_factory: nil) @extension_ui_handler = extension_ui @transport_factory = transport_factory || build_subprocess_factory(bin, args, env, cwd) @pending = {} @pending_mutex = Mutex.new @next_id = 0 @subscribers = [] @subscribers_mutex = Mutex.new @transport = nil @extension_ui = nil end |
Instance Attribute Details
#bin ⇒ Object (readonly)
Returns the value of attribute bin.
22 23 24 |
# File 'lib/pi_agent/client.rb', line 22 def bin @bin end |
Class Method Details
.resolve_bin(override = nil) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/pi_agent/client.rb', line 24 def self.resolve_bin(override = nil) candidate = override || ENV["PI_BIN"] || DEFAULT_BIN path = which(candidate) return path if path raise BinaryNotFoundError, <<~MSG Could not find the `pi` binary on PATH (looked for #{candidate.inspect}). Install with: npm install -g @earendil-works/pi-coding-agent@#{PiAgent::SUPPORTED_PI_VERSION} Or set PI_BIN to an explicit path. MSG end |
.which(cmd) ⇒ Object
37 38 39 40 41 42 43 44 45 46 |
# File 'lib/pi_agent/client.rb', line 37 def self.which(cmd) exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""] ENV["PATH"].to_s.split(File::PATH_SEPARATOR).each do |dir| exts.each do |ext| candidate = File.join(dir, "#{cmd}#{ext}") return candidate if File.executable?(candidate) && !File.directory?(candidate) end end nil end |
Instance Method Details
#alive? ⇒ Boolean
103 104 105 |
# File 'lib/pi_agent/client.rb', line 103 def alive? @transport&.alive? || false end |
#close ⇒ Object
95 96 97 98 99 100 101 |
# File 'lib/pi_agent/client.rb', line 95 def close # Drain extension UI handler threads while the transport is still # open so their responses can still be written. @extension_ui&.shutdown @transport&.close reject_pending(ProtocolError.new("Transport closed before response")) end |
#notify(type, params = {}) ⇒ Object
79 80 81 82 |
# File 'lib/pi_agent/client.rb', line 79 def notify(type, params = {}) payload = { type: type }.merge(params) @transport.write(payload) end |
#request(type, params = {}) ⇒ Object
70 71 72 73 74 75 76 77 |
# File 'lib/pi_agent/client.rb', line 70 def request(type, params = {}) id = next_id future = Future.new @pending_mutex.synchronize { @pending[id] = future } payload = { id: id, type: type }.merge(params) @transport.write(payload) future end |
#start ⇒ Object
60 61 62 63 64 65 66 67 68 |
# File 'lib/pi_agent/client.rb', line 60 def start @transport = @transport_factory.call( on_message: method(:handle_message), on_stderr: method(:handle_stderr) ) @extension_ui = ExtensionUI.new(writer: @transport, handler: @extension_ui_handler) @transport.start self end |
#subscribe(&block) ⇒ Object
84 85 86 87 88 89 |
# File 'lib/pi_agent/client.rb', line 84 def subscribe(&block) raise ArgumentError, "subscribe requires a block" unless block @subscribers_mutex.synchronize { @subscribers << block } block end |
#unsubscribe(handle) ⇒ Object
91 92 93 |
# File 'lib/pi_agent/client.rb', line 91 def unsubscribe(handle) @subscribers_mutex.synchronize { @subscribers.delete(handle) } end |