Class: Pikuri::Agent::ExtensionContext

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/agent/extension_context.rb

Overview

Capability facade handed to Pikuri::Agent::Extension#bind and Pikuri::Agent::Extension#on_user_message — the runtime counterpart of Configurator. Where the Configurator collects build-time declarations (tools, listeners, prompt snippets), this object grants the runtime capabilities an extension needs once the agent is fully wired: emitting domain events onto the listener stream, registering raw per-agent tools, and deriving sub-agent listener lists.

Why a handed object, not a getter on Agent

The Pikuri::Agent deliberately exposes NO public path to these capabilities — no listeners reader, no chat reader, no emit method. Holding an agent reference grants read access to its configuration (#tools, #transport, …) and nothing more; the write capabilities live here, and the only way to obtain this object is to be an Extension receiving a bind / on_user_message call (or to be handed it onward by one, e.g. SubAgent::SubAgentTool and Mcp::Servers::Connect both capture the context their extension’s bind received). Capabilities flow by explicit handoff, never by fetching from a globally reachable object.

The usual Ruby caveat applies: nothing here is mechanically sealed (instance_variable_get exists). The boundary is the API contract — same as every seam in CLAUDE.md.

Boundary rule

Operations that *act on* the live agent’s wiring live here. Passive readers of constructor-given config (transport, id, streaming, tools, cancellable, …) stay on Pikuri::Agent, reachable via #agent. Don’t move readers in; don’t add capabilities to Agent.

Audit

One context per agent, constructed by #initialize right before the extension bind sweep. ListenerList#emit has exactly two callers: Pikuri::Agent (loop narration) and this class (extension domain events). The roster of capability users is grep -rn ‘emit_event|add_raw_tool|sub_agent_listeners’ pikuri-*/lib/.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agent:, chat:, listeners:, on_close_sink:) ⇒ ExtensionContext

Returns a new instance of ExtensionContext.

Parameters:



57
58
59
60
61
62
# File 'lib/pikuri/agent/extension_context.rb', line 57

def initialize(agent:, chat:, listeners:, on_close_sink:)
  @agent = agent
  @chat = chat
  @listeners = listeners
  @on_close_handlers = on_close_sink
end

Instance Attribute Details

#agentAgent (readonly)

Returns the live agent, for read access to its configuration (tools, transport, id, streaming, …).

Returns:

  • (Agent)

    the live agent, for read access to its configuration (tools, transport, id, streaming, …).



66
67
68
# File 'lib/pikuri/agent/extension_context.rb', line 66

def agent
  @agent
end

Instance Method Details

#add_raw_tool(ruby_llm_tool) ⇒ void

This method returns an undefined value.

Register a raw RubyLLM::Tool subclass on the agent’s underlying chat, bypassing the Tool strict-validation seam — hence “raw”: native pikuri tools should go through Tool (registered at build time via Configurator#add_tool) so they get Tool::Parameters validation and the LLM-actionable “Error: …” contract. Intended callers: Mcp::Servers (MCP tools deliberately bypass — see IDEAS.md §“MCP tools bypass Pikuri::Tool entirely”) and SubAgent::Extension (the agent tool must register after the parent’s tool list is final).

The added tool does NOT enter Pikuri::Agent#tools, only the chat’s tool list. Sub-agents therefore cannot snapshot it — which is the whole point: activation is strictly per-agent, see IDEAS.md §“Per-agent activation, no propagation”.

Parameters:

  • ruby_llm_tool (Class)

    subclass of RubyLLM::Tool



109
110
111
112
# File 'lib/pikuri/agent/extension_context.rb', line 109

def add_raw_tool(ruby_llm_tool)
  @chat.with_tool(ruby_llm_tool)
  nil
end

#emit_event(event) ⇒ void

This method returns an undefined value.

Emit a domain event onto the agent’s listener stream.

Core Pikuri::Agent::Event variants narrate the chat loop and are emitted by Pikuri::Agent alone; gems define their own variants (e.g. Pikuri::Tasks::ListChanged) in their own namespace and emit them here. Listeners must no-op on variants they don’t recognize — Listener::Base#on_event‘s default and case-fallthrough give that for free.

Called on the agent’s thread (typically from inside a tool’s execute, where the event lands between Pikuri::Agent::Event::ToolCall and Pikuri::Agent::Event::ToolResult in the stream). Listeners doing cross-thread handoff snapshot/serialize inside on_event.

Parameters:

  • event (Object)

    an immutable event value (by convention a Data instance).



85
86
87
88
# File 'lib/pikuri/agent/extension_context.rb', line 85

def emit_event(event)
  @listeners.emit(event)
  nil
end

#on_close { ... } ⇒ void

This method returns an undefined value.

Register a handler called by Pikuri::Agent#close. Symmetric to Configurator#on_close — same LIFO + per-handler-rescue + idempotent semantics — but available post-construction, so an Pikuri::Agent::Extension‘s bind can install per-agent cleanup keyed to this specific agent (e.g. Pikuri::Memory::Extension arms its recorder’s bounded flush here).

Yields:

  • called with no arguments at close time

Raises:

  • (ArgumentError)


139
140
141
142
143
144
# File 'lib/pikuri/agent/extension_context.rb', line 139

def on_close(&blk)
  raise ArgumentError, 'on_close requires a block' unless block_given?

  @on_close_handlers << blk
  nil
end

#sub_agent_listeners(**params) ⇒ ListenerList

Derive a listener list for a spawned sub-agent via ListenerList#for_sub_agent. Sole intended caller: SubAgent::SubAgentTool, once per spawn.

The derived list deliberately aliases the parent’s listener instances where a listener opts to share by reference (stateful sinks like Listener::InMemoryEventList) — see ListenerList#for_sub_agent for the per-listener semantics.

Parameters:

  • params (Hash{Symbol => Object})

    forwarded to each listener’s for_sub_agent hook (currently id:).

Returns:



126
127
128
# File 'lib/pikuri/agent/extension_context.rb', line 126

def sub_agent_listeners(**params)
  @listeners.for_sub_agent(**params)
end