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

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:

Parameters:



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:

Parameters:



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.

Parameters:

  • ctx (ExtensionContext)

    the live agent being cleared (same instance bind received)



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.

Parameters:

  • ctx (ExtensionContext)

    the live agent whose turn this is (same instance bind received)

  • content (String)

    the user message (initial or interloper)

Returns:

  • (String, nil)

    text to inject verbatim as a system-role message after the user message, or nil to inject nothing



77
# File 'lib/pikuri/agent/extension.rb', line 77

def on_user_message(ctx, content); end

#system_prompt_snippetsArray<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.

Returns:

  • (Array<String>)

    zero or more prompt sections; [] for none



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.

Parameters:

  • tools (Array<Pikuri::Tool>)

    the agent's tools + sub-agent tools

Returns:



143
# File 'lib/pikuri/agent/extension.rb', line 143

def trifecta_contribution(tools) = nil