0.0
The project is in a healthy, maintained state
Ruby client for `@earendil-works/pi-coding-agent`. Spawns the pi CLI in RPC mode and speaks its JSONL protocol from Ruby. Designed for building interactive agent UIs (web, TUI) on top of pi. Not officially maintained by the pi project.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 2.0
~> 0.2
 Project Readme

pi-agent-rb

CI Gem Version License: MIT

Ruby client for the pi coding agent. Spawns pi --mode rpc and speaks its JSONL protocol from Ruby. Designed for building interactive agent UIs (web, TUI) on top of pi.

Not officially maintained by the pi project.

Requirements

  • Ruby 3.3+
  • pi 0.80.4+ on PATH (install via npm i -g @earendil-works/pi-coding-agent)
  • This gem is pinned against pi 0.82.1; other versions may work but are not verified.

Installation

Install the gem:

gem install pi-agent-rb

Or add it to your Gemfile:

gem "pi-agent-rb"

then run bundle install.

Quick start

require "pi_agent"

PiAgent.session do |session|
  session.prompt("Write a haiku about Ruby") do |event|
    print event.delta if event.type == :message_update
  end
end

A pi RPC process hosts one session, so there is no create/select step — PiAgent.session spawns pi --mode rpc and the session is that process.

prompt yields each Event until the agent fully finishes (agent_settled). Unlike agent_end, this includes any automatic retry, compaction retry, or queued continuation. Without a block it returns an Enumerator:

PiAgent.session do |session|
  events = session.prompt("List three primes")
  text = events.filter_map(&:delta).join
  puts text
end

For a single-shot call, run submits a prompt, drains the stream, and returns the final assistant text — pi's print mode:

PiAgent.session do |session|
  puts session.run("What's 2 + 2?")   # => "4"
end

Other session methods:

  • Prompting: steer, follow_up, events, abort
  • Model: set_model, cycle_model, available_models, set_thinking, available_thinking_levels
  • State: get_state, messages, last_assistant_text, session_stats
  • Context: compact
  • Sessions: new_session, switch_session, fork, clone_session, set_session_name

set_model accepts either set_model("anthropic/claude-sonnet-4-5") or set_model("anthropic", "claude-sonnet-4-5").

A prompt streams the complete session-level run (agent_start..agent_settled). Pass a block to follow_up for a sequential follow-up that starts immediately when idle (or queues when an agent is already running) and drains each Event through agent_settled:

PiAgent.session do |session|
  session.prompt("Draft a haiku") { |e| print e.delta if e.type == :message_update }
  session.follow_up("Now translate it to French") { |e| print e.delta if e.type == :message_update }
end

The block form is race-free: follow_up subscribes to the event stream before sending a prompt with pi's streamingBehavior: "followUp", so none of the cycle's events are missed. Use it only after the previous high-level stream has settled; pi events have no run IDs, so a session permits only one high-level event stream at a time. While a stream is active, use blockless follow_up to queue a continuation—the active stream will include it through agent_settled. The block form accepts plain agent input, not slash commands.

Pi emits agent_end after each low-level agent run, but may then retry, compact and retry, or process queued continuations. It emits agent_settled only when no automatic work remains, so high-level streams use that as their completion boundary. Upstream added the RPC event in pi 0.80.4; it is available in this gem's pinned pi 0.82.1.

events is a lower-level, prompt-less drain of the same stream. Because it subscribes lazily when iteration begins, it only works when you subscribe before the cycle starts — e.g. begin iterating it from a thread, then trigger the cycle. For the common follow-up case, prefer the block form above. prompt, block-form follow_up, and events are single-flight on a session; starting another before the current stream settles raises PiAgent::SessionError rather than consuming an unrelated settlement event.

Images

prompt, steer, and follow_up accept an images: array. Entries may be PiAgent::Image objects, file path strings, or raw ImageContent hashes, mixed freely:

PiAgent.session do |session|
  session.prompt("What's in these?", images: [
    "screenshot.png",                                  # path
    PiAgent::Image.from_file("diagram.jpg"),           # Image object
    PiAgent::Image.from_bytes(blob, mime_type: "image/webp")
  ]) { |e| ... }
end

Supported formats: png, jpeg, gif, webp.

For low-level RPC access (raw request/notify/subscribe), use PiAgent.open, which yields a PiAgent::Client.

Project trust

Since pi 0.79.0, project-local inputs (.pi/settings.json, project extensions, resources, and packages) are trust-gated. In RPC mode pi never prompts: unless the project was already trusted (e.g. interactively on the same machine), it silently ignores them. Pass approve: true to trust the project, or approve: false to explicitly ignore project inputs:

PiAgent.session(cwd: "/path/to/project", approve: true) do |session|
  # project .pi extensions and settings are loaded
end

Extension UI

pi extensions can request user interaction (confirm, select, input, editor) mid-run. Pass an extension_ui handler to answer them:

handler = lambda do |req|
  case req.method
  when :confirm then true              # confirmed
  when :select  then req.options.first # pick an option
  when :input   then "default"         # entered text
  when :editor  then req.prefill       # edited text
  # fire-and-forget (:notify, :set_status, ...) — return value ignored
  end
end

PiAgent.session(extension_ui: handler) do |session|
  session.prompt("Refactor the parser") { |e| ... }
end

Returning nil from a dialog handler cancels it. With no handler, dialogs are auto-cancelled so the agent never hangs. Handlers run on their own thread and never block the event stream.

Forking

PiAgent.session do |session|
  session.prompt("Add a feature") { |e| ... }

  # Branch from an earlier message
  forkable = session.fork_messages           # [{ "entryId" =>, "text" => }]
  session.fork(forkable.first["entryId"])    # => { "text" =>, "cancelled" => }

  session.clone_session                      # duplicate the active branch
  session.set_session_name("feature-work")
end

fork/clone_session return cancelled: true (rather than raising) if a pi extension vetoes the operation — that is an expected outcome, not an error.

Errors

  • A failed RPC command (success: false) raises PiAgent::CommandError, which carries the failing #command name.
  • Agent-side errors arrive in the event stream, not as exceptions — inspect them with Event#error?, #error_message, and #error_reason ("aborted" vs "error"). This covers extension_error events and errored assistant turns. The gem does not abort your iteration on agent errors; you decide how to react.

Protocol reference

The wire protocol is documented upstream in packages/coding-agent/docs/rpc.md.

Development

bin/setup        # bundle install
bundle exec rspec

License

MIT