Class: PiAgent::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/pi_agent/session.rb

Overview

High-level agent session. Wraps a Client and exposes prompt-flow ergonomics: submit a prompt and iterate the resulting event stream.

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

A pi RPC process hosts exactly one session, so there is no create/select step — the Session is the running pi process.

v1 limitation: ‘prompt` streams one agent cycle (agent_start..agent_end). A message queued with `follow_up` runs in a later cycle; pass a block to `follow_up` to drain that cycle race-free (it subscribes before sending). `events` is a prompt-less drain of the same stream for when you have already subscribed before the cycle starts. Bidirectional extension UI is not yet surfaced here.

Constant Summary collapse

DEFAULT_EVENT_TIMEOUT =

Max time to wait for the next event before assuming the agent stalled.

300
DEFAULT_ACK_TIMEOUT =

Max time to wait for a command to be acknowledged.

30

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client) ⇒ Session

Returns a new instance of Session.



30
31
32
# File 'lib/pi_agent/session.rb', line 30

def initialize(client)
  @client = client
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



28
29
30
# File 'lib/pi_agent/session.rb', line 28

def client
  @client
end

Instance Method Details

#abortObject

Abort the current agent run. Fire-and-forget.



108
109
110
111
# File 'lib/pi_agent/session.rb', line 108

def abort
  @client.notify("abort")
  self
end

#available_modelsObject

All configured models, as an array of Model hashes.



130
131
132
# File 'lib/pi_agent/session.rb', line 130

def available_models
  request_data("get_available_models").fetch("models", [])
end

#clone_sessionObject

Duplicate the current active branch into a new session at the current position. Returns { “cancelled” => bool }. Maps to the ‘clone` RPC command (named `clone_session` to avoid shadowing Object#clone).



202
203
204
# File 'lib/pi_agent/session.rb', line 202

def clone_session
  request_data("clone")
end

#closeObject



211
212
213
# File 'lib/pi_agent/session.rb', line 211

def close
  @client.close
end

#compact(custom_instructions: nil) ⇒ Object

Manually compact the conversation context to reduce token usage. Returns the result hash ({ “summary” =>, “firstKeptEntryId” =>, “tokensBefore” => }).



158
159
160
161
162
# File 'lib/pi_agent/session.rb', line 158

def compact(custom_instructions: nil)
  params = {}
  params[:customInstructions] = custom_instructions if custom_instructions
  request_data("compact", params)
end

#cycle_modelObject

Switch to the next configured model. Returns the new { “model” =>, “thinkingLevel” =>, “isScoped” => } hash, or {} when only one model is available.



125
126
127
# File 'lib/pi_agent/session.rb', line 125

def cycle_model
  request_data("cycle_model")
end

#events(event_timeout: DEFAULT_EVENT_TIMEOUT, &block) ⇒ Object

Drain the event stream without submitting a new prompt. With a block, yields each Event until the cycle finishes (agent_end) and returns self; without a block, returns an Enumerator of Events.

The subscription is established lazily, when iteration begins — so any cycle triggered before you call ‘events` may have already emitted events that are then missed. To drain a `follow_up` cycle race-free, pass a block to `follow_up` instead. Use `events` only when you subscribe before the cycle starts (e.g. from a thread that begins iterating, then trigger the cycle). With nothing queued, it blocks for `event_timeout` (300s default).



60
61
62
63
64
65
66
67
# File 'lib/pi_agent/session.rb', line 60

def events(event_timeout: DEFAULT_EVENT_TIMEOUT, &block)
  stream = subscribed_stream(event_timeout: event_timeout)

  return stream unless block

  stream.each(&block)
  self
end

#follow_up(message, images: nil, event_timeout: DEFAULT_EVENT_TIMEOUT, &block) ⇒ Object

Queue a follow-up message, delivered only after the agent stops.

Without a block this is fire-and-forget: it queues the message and returns self. With a block it drains the resulting agent cycle race-free — the subscription is established before the message is sent, so no events are missed — yielding each Event until agent_end and returning self. Prefer the block form to consume a follow-up; the standalone ‘events` drain only works if you subscribe first.



85
86
87
88
89
90
91
92
93
94
# File 'lib/pi_agent/session.rb', line 85

def follow_up(message, images: nil, event_timeout: DEFAULT_EVENT_TIMEOUT, &block)
  params = message_params(message, images)
  unless block
    @client.request("follow_up", params).value!(timeout: DEFAULT_ACK_TIMEOUT)
    return self
  end

  event_stream("follow_up", params, event_timeout: event_timeout).each(&block)
  self
end

#fork(entry_id) ⇒ Object

Fork a new branch from a previous user message (an entryId from ‘fork_messages`). Returns { “text” => <forked-from text>, “cancelled” => bool }; `cancelled` is true if an extension vetoed it.



194
195
196
# File 'lib/pi_agent/session.rb', line 194

def fork(entry_id)
  request_data("fork", entryId: entry_id)
end

#fork_messagesObject

List user messages available for forking. Returns an array of { “entryId” => …, “text” => … } hashes.



187
188
189
# File 'lib/pi_agent/session.rb', line 187

def fork_messages
  request_data("get_fork_messages").fetch("messages", [])
end

#get_stateObject



141
142
143
# File 'lib/pi_agent/session.rb', line 141

def get_state
  @client.request("get_state").value!(timeout: DEFAULT_ACK_TIMEOUT)
end

#last_assistant_textObject

Text of the last assistant message, or nil if there is none.



151
152
153
# File 'lib/pi_agent/session.rb', line 151

def last_assistant_text
  request_data("get_last_assistant_text")["text"]
end

#messagesObject

Full conversation history, as an array of AgentMessage hashes.



146
147
148
# File 'lib/pi_agent/session.rb', line 146

def messages
  request_data("get_messages").fetch("messages", [])
end

#new_session(parent_session: nil) ⇒ Object

Start a fresh session in the same pi process. Pass ‘parent_session:` (a session file path) to record provenance. Returns { “cancelled” => bool }; cancelled is true if an extension vetoed it.



167
168
169
170
171
# File 'lib/pi_agent/session.rb', line 167

def new_session(parent_session: nil)
  params = {}
  params[:parentSession] = parent_session if parent_session
  request_data("new_session", params)
end

#prompt(message, images: nil, event_timeout: DEFAULT_EVENT_TIMEOUT, &block) ⇒ Object

Submit a user prompt. With a block, yields each Event until the agent finishes (agent_end), then returns self. Without a block, returns an Enumerator of Events.

‘images` accepts PiAgent::Image objects, file path strings, or raw ImageContent hashes — in any mix.



40
41
42
43
44
45
46
47
# File 'lib/pi_agent/session.rb', line 40

def prompt(message, images: nil, event_timeout: DEFAULT_EVENT_TIMEOUT, &block)
  stream = event_stream("prompt", message_params(message, images), event_timeout: event_timeout)

  return stream unless block

  stream.each(&block)
  self
end

#run(message, images: nil, event_timeout: DEFAULT_EVENT_TIMEOUT) ⇒ Object

Single-shot helper mirroring pi’s print mode: submit ‘message`, drain the whole event stream, and return the final assistant text (nil if the agent produced none). Yields each Event to an optional block while the stream drains.



100
101
102
103
104
105
# File 'lib/pi_agent/session.rb', line 100

def run(message, images: nil, event_timeout: DEFAULT_EVENT_TIMEOUT)
  prompt(message, images: images, event_timeout: event_timeout) do |event|
    yield event if block_given?
  end
  last_assistant_text
end

#session_statsObject

Token usage, cost, and context-window stats for the current session. Returns the data hash, including “sessionId” and “sessionFile”.



181
182
183
# File 'lib/pi_agent/session.rb', line 181

def session_stats
  request_data("get_session_stats")
end

#set_model(provider, model_id = nil) ⇒ Object

Switch to a specific model. Accepts either a single “provider/modelId” string or the two parts as separate arguments.



115
116
117
118
119
120
# File 'lib/pi_agent/session.rb', line 115

def set_model(provider, model_id = nil)
  provider, model_id = provider.split("/", 2) if model_id.nil?
  @client.request("set_model", provider: provider, modelId: model_id)
         .value!(timeout: DEFAULT_ACK_TIMEOUT)
  self
end

#set_session_name(name) ⇒ Object



206
207
208
209
# File 'lib/pi_agent/session.rb', line 206

def set_session_name(name)
  @client.request("set_session_name", name: name).value!(timeout: DEFAULT_ACK_TIMEOUT)
  self
end

#set_thinking(level) ⇒ Object

Set the reasoning level: “off”, “minimal”, “low”, “medium”, “high”, or “xhigh” (xhigh is OpenAI codex-max only).



136
137
138
139
# File 'lib/pi_agent/session.rb', line 136

def set_thinking(level)
  @client.request("set_thinking_level", level: level).value!(timeout: DEFAULT_ACK_TIMEOUT)
  self
end

#steer(message, images: nil) ⇒ Object

Queue a steering message while the agent is running. Delivered after the current assistant turn finishes its tool calls, before the next LLM call. Fire-and-forget; raises on rejection.



72
73
74
75
# File 'lib/pi_agent/session.rb', line 72

def steer(message, images: nil)
  @client.request("steer", message_params(message, images)).value!(timeout: DEFAULT_ACK_TIMEOUT)
  self
end

#switch_session(path) ⇒ Object

Load a different session file into this process. Returns { “cancelled” => bool }; cancelled is true if an extension vetoed it.



175
176
177
# File 'lib/pi_agent/session.rb', line 175

def switch_session(path)
  request_data("switch_session", sessionPath: path)
end