pi-agent-rb 0.1.18 → 0.2.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: a86ab7926d2a79fcd0b1777fa3e646762e2940b570a2b3b23ec05f1b6a2b61ad
4
- data.tar.gz: cbc2d4df9b4999003e0b2733d190beb0f972997478d3365ecd14770602440c8a
3
+ metadata.gz: 24f258d02d946aa8752944f4438581126665c94b5ee1253e61c8ebd16ecb6596
4
+ data.tar.gz: f98727ad546acbc6ea8dee3e2cf52af1cf7694f3875181fdec16c3773fd2e0b7
5
5
  SHA512:
6
- metadata.gz: dc0fc51144b3820a94753bede3a0f3f14df30cf1b16bce2e4e87530b1a3fb7f9dcc422910ceefcd3ccff42a7978a4c4f81ccc5182606be89aa176cc4375819c9
7
- data.tar.gz: 88100dce5b222fde9d481a402190dc1d6dbb205de75ee8f2528a95b697e752ed68696f3c642dabbb749bd00a9bf1e76a8a8846fe7ded16227e0e300bdfceb8f8
6
+ metadata.gz: 32e5f3a97a2f6b890a1f1c8aecdd59125a4588b6879e815471ad2257942d291a6ccc5c46d32e4bd04abe13020e8ccb4cde1643dfb9230bea9e7974bb8008513f
7
+ data.tar.gz: '099b2a2267bd8a7212c6d90473775d54a22e7d80982471682ae339cd01875383561bd77364f9f9fe54c8a9514ae26023811ec24a8bcbe1b2dd035f1412f30377'
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-07-23
11
+
12
+ Minor (not patch) release: besides the stream-truncation fix, the block form
13
+ of `Session#follow_up` and stream concurrency behave differently (see
14
+ Changed).
15
+
16
+ ### Fixed
17
+ - `Session#prompt`, `#follow_up`, `#events`, and `#run` now drain through
18
+ `agent_settled` instead of stopping at the first `agent_end`. Since pi
19
+ 0.80.4, `agent_end` marks only one low-level run and may be followed by an
20
+ automatic retry, compaction retry, or queued continuation; stopping there
21
+ could unsubscribe early and silently drop the final response events.
22
+
23
+ ### Changed
24
+ - The block form of `Session#follow_up` now uses pi's streaming-aware `prompt`
25
+ command, so sequential follow-ups start when the session is idle while
26
+ the blockless form remains a direct, queue-only `follow_up` RPC command for
27
+ active runs. High-level streams are now single-flight per session because
28
+ pi events have no run IDs; overlapping streams raise `SessionError` instead
29
+ of potentially consuming another run's `agent_settled` event.
30
+
10
31
  ## [0.1.18] - 2026-07-21
11
32
 
12
33
  ### Added
data/README.md CHANGED
@@ -13,7 +13,7 @@ building interactive agent UIs (web, TUI) on top of pi.
13
13
  ## Requirements
14
14
 
15
15
  - Ruby 3.3+
16
- - `pi` on `PATH` (install via `npm i -g @earendil-works/pi-coding-agent`)
16
+ - `pi` 0.80.4+ on `PATH` (install via `npm i -g @earendil-works/pi-coding-agent`)
17
17
  - This gem is pinned against pi `0.81.1`; other versions may work but are not verified.
18
18
 
19
19
  ## Installation
@@ -47,8 +47,10 @@ end
47
47
  A pi RPC process hosts one session, so there is no create/select step —
48
48
  `PiAgent.session` spawns `pi --mode rpc` and the session *is* that process.
49
49
 
50
- `prompt` yields each [`Event`](lib/pi_agent/event.rb) until the agent
51
- finishes (`agent_end`). Without a block it returns an `Enumerator`:
50
+ `prompt` yields each [`Event`](lib/pi_agent/event.rb) until the agent fully
51
+ finishes (`agent_settled`). Unlike `agent_end`, this includes any automatic
52
+ retry, compaction retry, or queued continuation. Without a block it returns
53
+ an `Enumerator`:
52
54
 
53
55
  ```ruby
54
56
  PiAgent.session do |session|
@@ -79,9 +81,10 @@ Other session methods:
79
81
  `set_model` accepts either `set_model("anthropic/claude-sonnet-4-5")` or
80
82
  `set_model("anthropic", "claude-sonnet-4-5")`.
81
83
 
82
- A `prompt` streams one agent cycle (`agent_start`..`agent_end`). A message
83
- queued with `follow_up` runs in a *later* cycle; pass a block to `follow_up`
84
- to drain that cycle. Like `prompt`, it yields each `Event` until `agent_end`:
84
+ A `prompt` streams the complete session-level run
85
+ (`agent_start`..`agent_settled`). Pass a block to `follow_up` for a sequential
86
+ follow-up that starts immediately when idle (or queues when an agent is
87
+ already running) and drains each `Event` through `agent_settled`:
85
88
 
86
89
  ```ruby
87
90
  PiAgent.session do |session|
@@ -90,14 +93,27 @@ PiAgent.session do |session|
90
93
  end
91
94
  ```
92
95
 
93
- The block form is race-free: `follow_up` subscribes to the event stream
94
- *before* sending the message, so none of the cycle's events are missed.
96
+ The block form is race-free: `follow_up` subscribes to the event stream before
97
+ sending a `prompt` with pi's `streamingBehavior: "followUp"`, so none of the
98
+ cycle's events are missed. Use it only after the previous high-level stream has
99
+ settled; pi events have no run IDs, so a session permits only one high-level
100
+ event stream at a time. While a stream is active, use blockless `follow_up` to
101
+ queue a continuation—the active stream will include it through
102
+ `agent_settled`. The block form accepts plain agent input, not slash commands.
103
+
104
+ Pi emits `agent_end` after each low-level agent run, but may then retry,
105
+ compact and retry, or process queued continuations. It emits
106
+ `agent_settled` only when no automatic work remains, so high-level streams
107
+ use that as their completion boundary. Upstream added the RPC event in pi
108
+ 0.80.4; it is available in this gem's pinned pi 0.81.1.
95
109
 
96
110
  `events` is a lower-level, prompt-less drain of the same stream. Because it
97
111
  subscribes lazily when iteration begins, it only works when you subscribe
98
112
  *before* the cycle starts — e.g. begin iterating it from a thread, then
99
113
  trigger the cycle. For the common follow-up case, prefer the block form
100
- above.
114
+ above. `prompt`, block-form `follow_up`, and `events` are single-flight on a
115
+ session; starting another before the current stream settles raises
116
+ `PiAgent::SessionError` rather than consuming an unrelated settlement event.
101
117
 
102
118
  ### Images
103
119
 
@@ -6,12 +6,13 @@ module PiAgent
6
6
  # dedicated accessor yet.
7
7
  #
8
8
  # Event types are exposed as Ruby symbols (e.g. `:text_delta`,
9
- # `:agent_end`) matching the upstream protocol's `type` field.
9
+ # `:agent_settled`) matching the upstream protocol's `type` field.
10
10
  class Event
11
11
  # Event types that terminate a single prompt's event stream.
12
- # `agent_end` fires when the agent finishes processing the current
13
- # prompt cycle; we stop iterating then.
14
- TERMINAL_TYPES = %i[agent_end].freeze
12
+ # `agent_end` only finishes one low-level run; retries, compaction, or
13
+ # queued continuations may follow. `agent_settled` is emitted once all
14
+ # automatic work has finished.
15
+ TERMINAL_TYPES = %i[agent_settled].freeze
15
16
 
16
17
  attr_reader :raw, :type
17
18
 
@@ -13,12 +13,12 @@ module PiAgent
13
13
  # A pi RPC process hosts exactly one session, so there is no
14
14
  # create/select step — the Session *is* the running pi process.
15
15
  #
16
- # v1 limitation: `prompt` streams one agent cycle (agent_start..agent_end).
17
- # A message queued with `follow_up` runs in a later cycle; pass a block to
18
- # `follow_up` to drain that cycle race-free (it subscribes before sending).
19
- # `events` is a prompt-less drain of the same stream for when you have
20
- # already subscribed before the cycle starts. Bidirectional extension UI
21
- # is not yet surfaced here.
16
+ # `prompt` streams until the session-level run settles, including retries,
17
+ # automatic compaction retries, and queued continuations. A message queued
18
+ # with blockless `follow_up` runs after an active agent; pass a block to
19
+ # submit it as a streaming-aware prompt and drain it race-free. `events` is
20
+ # a prompt-less drain for when you have already subscribed before processing
21
+ # starts.
22
22
  class Session
23
23
  # Max time to wait for the next event before assuming the agent stalled.
24
24
  DEFAULT_EVENT_TIMEOUT = 300
@@ -29,11 +29,14 @@ module PiAgent
29
29
 
30
30
  def initialize(client)
31
31
  @client = client
32
+ @stream_state_mutex = Mutex.new
33
+ @event_stream_active = false
32
34
  end
33
35
 
34
- # Submit a user prompt. With a block, yields each Event until the
35
- # agent finishes (agent_end), then returns self. Without a block,
36
- # returns an Enumerator of Events.
36
+ # Submit a user prompt. With a block, yields each Event until the agent
37
+ # fully settles (agent_settled), then returns self. This includes any
38
+ # automatic retry, compaction retry, or queued continuation. Without a
39
+ # block, returns an Enumerator of Events.
37
40
  #
38
41
  # `images` accepts PiAgent::Image objects, file path strings, or
39
42
  # raw ImageContent hashes — in any mix.
@@ -47,8 +50,8 @@ module PiAgent
47
50
  end
48
51
 
49
52
  # Drain the event stream without submitting a new prompt. With a block,
50
- # yields each Event until the cycle finishes (agent_end) and returns
51
- # self; without a block, returns an Enumerator of Events.
53
+ # yields each Event until the run fully settles (agent_settled) and
54
+ # returns self; without a block, returns an Enumerator of Events.
52
55
  #
53
56
  # The subscription is established lazily, when iteration begins — so any
54
57
  # cycle triggered *before* you call `events` may have already emitted
@@ -76,12 +79,12 @@ module PiAgent
76
79
 
77
80
  # Queue a follow-up message, delivered only after the agent stops.
78
81
  #
79
- # Without a block this is fire-and-forget: it queues the message and
80
- # returns self. With a block it drains the resulting agent cycle
81
- # race-free the subscription is established *before* the message is
82
- # sent, so no events are missed yielding each Event until agent_end
83
- # and returning self. Prefer the block form to consume a follow-up;
84
- # the standalone `events` drain only works if you subscribe first.
82
+ # Without a block this maps to pi's raw `follow_up` command: it only queues
83
+ # the message and returns self, so call it while an agent run is active.
84
+ # With a block it uses pi's streaming-aware `prompt` command, which starts
85
+ # a run when idle or queues a follow-up when busy. The subscription is
86
+ # established before sending, then yields through agent_settled and returns
87
+ # self. Prefer this form for a sequential, consumable follow-up.
85
88
  def follow_up(message, images: nil, event_timeout: DEFAULT_EVENT_TIMEOUT, &block)
86
89
  params = message_params(message, images)
87
90
  unless block
@@ -89,7 +92,12 @@ module PiAgent
89
92
  return self
90
93
  end
91
94
 
92
- event_stream("follow_up", params, event_timeout: event_timeout).each(&block)
95
+ if message.start_with?("/")
96
+ raise SessionError, "Block-form follow_up does not support slash commands; use prompt instead"
97
+ end
98
+
99
+ params[:streamingBehavior] = "followUp"
100
+ event_stream("prompt", params, event_timeout: event_timeout).each(&block)
93
101
  self
94
102
  end
95
103
 
@@ -280,17 +288,36 @@ module PiAgent
280
288
  # iteration of the returned Enumerator so cleanup is deterministic.
281
289
  def subscribed_stream(event_timeout:, &before_pump)
282
290
  Enumerator.new do |yielder|
283
- queue = Queue.new
284
- handle = @client.subscribe { |msg| queue << msg }
291
+ claim_event_stream!
292
+
293
+ handle = nil
285
294
  begin
295
+ queue = Queue.new
296
+ handle = @client.subscribe { |msg| queue << msg }
286
297
  before_pump&.call
287
298
  pump_events(queue, yielder, event_timeout)
288
299
  ensure
289
- @client.unsubscribe(handle)
300
+ @client.unsubscribe(handle) if handle
301
+ release_event_stream
290
302
  end
291
303
  end
292
304
  end
293
305
 
306
+ def claim_event_stream!
307
+ claimed = @stream_state_mutex.synchronize do
308
+ next false if @event_stream_active
309
+
310
+ @event_stream_active = true
311
+ end
312
+ return if claimed
313
+
314
+ raise SessionError, "Another event stream is active; queue with blockless follow_up or wait for it to settle"
315
+ end
316
+
317
+ def release_event_stream
318
+ @stream_state_mutex.synchronize { @event_stream_active = false }
319
+ end
320
+
294
321
  def pump_events(queue, yielder, event_timeout)
295
322
  loop do
296
323
  msg = queue.pop(timeout: event_timeout)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PiAgent
4
- VERSION = "0.1.18"
4
+ VERSION = "0.2.0"
5
5
 
6
6
  # Pinned upstream pi-coding-agent version this gem is verified against.
7
7
  # See: https://www.npmjs.com/package/@earendil-works/pi-coding-agent
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pi-agent-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.18
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chagel