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, this grants the runtime capabilities an extension needs once the agent is wired: emitting domain events, registering raw per-agent tools, deriving sub-agent listener lists.

Why a handed object, not a getter on Agent

Pikuri::Agent exposes NO public path to these capabilities (no listeners / chat reader, no emit method): holding an agent grants read access to its config and nothing more. The only way to obtain this object is to be an Extension receiving bind / on_user_message — or to be handed it onward by one (SubAgent::SubAgentTool and Mcp::Servers::Connect both capture the context their bind received). Capabilities flow by explicit handoff, never by fetching from a globally reachable object. The boundary is the API contract, not a mechanical seal — 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, tools, ...) stay on Pikuri::Agent, reachable via #agent. Don't move readers in; don't add capabilities to Agent.

Audit

One context per agent, built by #initialize before the bind sweep. ListenerList#emit has exactly two callers: Pikuri::Agent (loop narration) and this class (domain events). Capability-user roster: 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:



46
47
48
49
50
51
# File 'lib/pikuri/agent/extension_context.rb', line 46

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, ...).



55
56
57
# File 'lib/pikuri/agent/extension_context.rb', line 55

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 chat, bypassing the Tool strict-validation seam — hence "raw" (native tools go through Configurator#add_tool for Tool::Parameters validation). Callers: Mcp::Servers (MCP tools deliberately bypass) and SubAgent::Extension (the agent tool registers after the parent's tool list is final).

The tool does NOT enter Pikuri::Agent#tools, only the chat's list — so sub-agents can't snapshot it, which is the point: activation is strictly per-agent.

Parameters:

  • ruby_llm_tool (Class)

    subclass of RubyLLM::Tool



87
88
89
90
# File 'lib/pikuri/agent/extension_context.rb', line 87

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 loop (Pikuri::Agent-emitted); gems define their own (e.g. Pikuri::Tasks::ListChanged) and emit them here. Listeners must no-op on variants they don't recognize (the Listener::Base#on_event default gives that for free).

Called on the agent's thread (typically inside a tool's execute, landing between Pikuri::Agent::Event::ToolCall and Pikuri::Agent::Event::ToolResult); listeners doing cross-thread handoff snapshot inside on_event.

Parameters:

  • event (Object)

    an immutable event value (by convention a Data)



69
70
71
72
# File 'lib/pikuri/agent/extension_context.rb', line 69

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 (LIFO, per-handler rescue, idempotent) but available post-construction, so an Pikuri::Agent::Extension's bind can install per-agent cleanup (Memory arms its recorder's bounded flush here).

Yields:

  • called with no arguments at close time

Raises:

  • (ArgumentError)


112
113
114
115
116
117
# File 'lib/pikuri/agent/extension_context.rb', line 112

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 caller: SubAgent::SubAgentTool, once per spawn. The derived list aliases the parent's listener instances where a listener opts to share by reference — see ListenerList#for_sub_agent.

Parameters:

  • params (Hash{Symbol => Object})

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

Returns:



101
102
103
# File 'lib/pikuri/agent/extension_context.rb', line 101

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