Module: Pikuri::Agent::Extension
- Defined in:
- lib/pikuri/agent/extension.rb
Overview
The Extension protocol — how hosts bolt extra capabilities
(system-prompt sections, tools, lifecycle hooks) onto an Pikuri::Agent.
Added via Configurator#add_extension inside the Agent.new block;
the Agent then drives a fixed hook set: #configure during the block
(gets the Configurator), #bind once fully constructed,
#on_user_message per user turn, #on_conversation_reset per
#clear_conversation (these three get the runtime
ExtensionContext), and #system_prompt_snippets whenever the prompt
is (re)assembled (no argument — reads state set in configure).
Mix this module in to inherit no-op defaults for every hook, and
override what you need. Not including it also works if the class
defines every hook — the Agent calls them by name with no
respond_to? guard, so a missing one raises.
The whole surface fires on the parent agent only — sub-agents do not inherit extensions (each persona owns its toolset and prompt verbatim).
Example
class MyExtension
include Pikuri::Agent::Extension
def system_prompt_snippets = ["Always be polite."]
# configure / bind not overridden — inherit the empty defaults
end
See Pikuri::Mcp::Extension / Pikuri::Skill::Extension for worked
implementations.
Instance Method Summary collapse
-
#bind(ctx) ⇒ void
Called by #initialize after the block returns and the chat is fully wired, with the agent's ExtensionContext.
-
#configure(c) ⇒ void
Called immediately by Configurator#add_extension during the
Agent.newblock, once per instance, with the parent's Configurator. -
#on_conversation_reset(ctx) ⇒ void
Optional hook fired by #clear_conversation (a "/clear").
-
#on_user_message(ctx, content) ⇒ String?
Optional per-turn hook fired after a user message is added to the chat.
-
#system_prompt_snippets ⇒ Array<String>
This extension's system-prompt contributions, as text sections.
-
#trifecta_contribution(tools) ⇒ Pikuri::Trifecta::Contribution?
What this extension adds to the lethal-trifecta tree (Trifecta): legs the wired agent holds beyond its own tools, and child nodes for any sub-agents it introduces.
Instance Method Details
#bind(ctx) ⇒ void
This method returns an undefined value.
Called by Pikuri::Agent#initialize after the block returns and the chat is fully wired, with the agent's Pikuri::Agent::ExtensionContext. Default no-op; override to install state keyed to the live agent — typically:
- dynamic tools via Pikuri::Agent::ExtensionContext#add_raw_tool
- domain-event wiring via Pikuri::Agent::ExtensionContext#emit_event
- per-agent
on_closevia Pikuri::Agent::ExtensionContext#on_close - stash
ctxif the extension's tools act on this agent later
59 |
# File 'lib/pikuri/agent/extension.rb', line 59 def bind(ctx); end |
#configure(c) ⇒ void
This method returns an undefined value.
Called immediately by Configurator#add_extension during the
Agent.new block, once per instance, with the parent's
Configurator. Default no-op; override to install agent-agnostic
state — typically:
- tools via Configurator#add_tool, listeners via Configurator#add_listener
- parent-only
on_closecleanup via Configurator#on_close - read transport / cancellable / etc. off the Configurator
46 |
# File 'lib/pikuri/agent/extension.rb', line 46 def configure(c); end |
#on_conversation_reset(ctx) ⇒ void
This method returns an undefined value.
Optional hook fired by Pikuri::Agent#clear_conversation (a "/clear"). Override to drop conversation-scoped state; default no-op, so an extension holding only process/infrastructure state opts out by not defining it.
That distinction is the point: reset state that only makes sense within one conversation (+Pikuri::Tasks::Extension+ clears its list, the workspace clears its read-record); leave infrastructure alone (MCP subprocesses, a docker-backed server, the memory recorder's queue — a clear is not a quit).
Do not touch the system prompt here — that refreshes automatically
via #system_prompt_snippets, which the clear re-pulls. This hook is
only for imperative state the prompt machinery can't express. An
extension owning a domain event should emit it here so UI listeners
see the reset (Tasks fires Tasks::ListChanged with an empty list),
synchronously after the Agent's Pikuri::Agent::Event::Reset.
100 |
# File 'lib/pikuri/agent/extension.rb', line 100 def on_conversation_reset(ctx); end |
#on_user_message(ctx, content) ⇒ String?
Optional per-turn hook fired after a user message is added to the
chat. Default no-op returning nil; override and return a String to
have it appended after the user turn as a <system-reminder> reference
block (see Pikuri::Agent#append_reference_block for the envelope and why it
is not a :system message).
The Agent only ever appends the returned block at the tail — never rewrites or removes an earlier one, which would bust the provider prefix cache for everything after the edit. Stale blocks ride the existing context-window machinery, not a per-turn rewrite.
77 |
# File 'lib/pikuri/agent/extension.rb', line 77 def (ctx, content); end |
#system_prompt_snippets ⇒ Array<String>
This extension's system-prompt contributions, as text sections. Default none (+[]+); override to contribute one or more.
Pull, not push: the Pikuri::Agent assembles its prompt by concatenating the
base with what every extension returns here — at construction and
again on every Pikuri::Agent#clear_conversation. So a section computed from
live state (a resident memory persona, MACHINE.md from disk) is
recomputed on clear and stays current; a static section returns the
same constant.
No argument: read instance state populated in #configure (which runs
first). Producing that state (starting servers, probing a model)
belongs in configure; this only reads it, so it stays cheap enough
for every clear (memoize an expensive one-shot at its source —
Pikuri::Os::SystemInfo#prompt_section). Sections are joined by the
Agent (blank/nil dropped), in registration order.
120 |
# File 'lib/pikuri/agent/extension.rb', line 120 def system_prompt_snippets = [] |
#trifecta_contribution(tools) ⇒ Pikuri::Trifecta::Contribution?
What this extension adds to the lethal-trifecta tree (Trifecta): legs the wired agent holds beyond its own tools, and child nodes for any sub-agents it introduces. Default none.
Only two kinds of extension owe an answer, and both are things per-tool tagging structurally cannot see: one that introduces sub-agents (a whole node, plus the gate on the delegation edge) and one that mounts a foreign tool surface whose tools carry no legs of their own.
tools is the wired agent's tools plus its sub-agent tools, so an
extension resolving a persona's tool_names can select the same
objects the sub-agent will actually receive — the resolution and the
node it produces stay with whoever owns them.
Unlike every other hook here, the Agent calls this guarded by +respond_to?+: this protocol permits a class that defines all hooks without including the module, and an unguarded new hook would break those. So a non-including extension may simply not have it.
143 |
# File 'lib/pikuri/agent/extension.rb', line 143 def trifecta_contribution(tools) = nil |