Class: Pikuri::Agent::ExtensionContext
- Inherits:
-
Object
- Object
- Pikuri::Agent::ExtensionContext
- 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
-
#agent ⇒ Agent
readonly
The live agent, for read access to its configuration (tools, transport, id, streaming, …).
Instance Method Summary collapse
-
#add_raw_tool(ruby_llm_tool) ⇒ void
Register a raw
RubyLLM::Toolsubclass 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. -
#emit_event(event) ⇒ void
Emit a domain event onto the agent’s listener stream.
-
#initialize(agent:, chat:, listeners:, on_close_sink:) ⇒ ExtensionContext
constructor
A new instance of ExtensionContext.
-
#on_close { ... } ⇒ void
Register a handler called by #close.
-
#sub_agent_listeners(**params) ⇒ ListenerList
Derive a listener list for a spawned sub-agent via ListenerList#for_sub_agent.
Constructor Details
#initialize(agent:, chat:, listeners:, on_close_sink:) ⇒ ExtensionContext
Returns a new instance of ExtensionContext.
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
#agent ⇒ Agent (readonly)
Returns 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”.
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.
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).
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.
126 127 128 |
# File 'lib/pikuri/agent/extension_context.rb', line 126 def sub_agent_listeners(**params) @listeners.for_sub_agent(**params) end |