Class: Pikuri::Skill::PathActivationListener
- Inherits:
-
Agent::Listener::Base
- Object
- Agent::Listener::Base
- Pikuri::Skill::PathActivationListener
- Defined in:
- lib/pikuri/skill/path_activation_listener.rb
Overview
Watches the tool stream for a file the model is about to touch, and promotes the +paths:+-gated skills that file fires:
listener = PathActivationListener.new(
activation: activation,
root: project_root,
on_promote: ->(skills, path) { ext.announce(skills, path) }
)
Nothing fires twice: Activation#activate answers with the skills that
were not already in play, and on_promote is called only when that list
is non-empty.
Implementation details
TOUCH_TOOLS is an allowlist of tools, not a convention about argument
names. read, write and edit agree on path — but so do grep and
glob, whose path is a search root, and one repo-root grep would
otherwise activate half the catalog at once. That is the same
supplied-vs-walked line the Denylist seam draws for the credential
denylist, from the other direction.
Activation reads the model's arguments, at before_tool_call, so a
read of a path that does not exist, or a write the human then rejects,
promotes anyway. Deliberate: over-firing costs a few hundred bytes,
under-firing withholds the skill at the moment it was wanted. It is also
what buys the in-turn landing — an enqueue here drains as soon as the
running tool batch is answered, with no extra round-trip.
The path is model-supplied, so it is resolved against root lexically
— no realpath, or a write to a file that does not exist yet would
never match — and anything landing outside the root is dropped rather
than matched against the wrong tree.
Constant Summary collapse
- TOUCH_TOOLS =
Tools whose
pathargument names a file the model means to touch. %w[read write edit].freeze
Instance Method Summary collapse
-
#for_sub_agent ⇒ nil
A sub-agent must never run this: it holds the parent's activation, so a child reading a file would mutate the parent's gate state and enqueue onto the parent's interloper.
-
#initialize(activation:, root:, on_promote:) ⇒ PathActivationListener
constructor
A new instance of PathActivationListener.
- #on_event(event) ⇒ void
Constructor Details
#initialize(activation:, root:, on_promote:) ⇒ PathActivationListener
Returns a new instance of PathActivationListener.
49 50 51 52 53 54 |
# File 'lib/pikuri/skill/path_activation_listener.rb', line 49 def initialize(activation:, root:, on_promote:) super() @activation = activation @root = root @on_promote = on_promote end |
Instance Method Details
#for_sub_agent ⇒ nil
A sub-agent must never run this: it holds the parent's activation, so a child reading a file would mutate the parent's gate state and enqueue onto the parent's interloper. Sub-agents get no promotions at all — Activation::Sealed is the other half of that.
78 |
# File 'lib/pikuri/skill/path_activation_listener.rb', line 78 def for_sub_agent(**) = nil |
#on_event(event) ⇒ void
This method returns an undefined value.
59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/pikuri/skill/path_activation_listener.rb', line 59 def on_event(event) return unless event.is_a?(Pikuri::Agent::Event::ToolCall) return unless TOUCH_TOOLS.include?(event.name) path = workspace_relative(event.arguments['path'] || event.arguments[:path]) return if path.nil? fired = @activation.catalog.list.select { |skill| PathMatcher.match?(skill.paths, path) } promoted = @activation.activate(fired) @on_promote.call(promoted, path) unless promoted.empty? nil end |