Module: Protege::ToolMixin
- Included in:
- Tool
- Defined in:
- lib/protege/extensions/tool_mixin.rb
Overview
Contract mixin for harness tools — the surface the agent uses to take action within the LOGI
Orchestrator. Concrete tools subclass Protege::Tool (which includes this module), declare
their description and JSON Schema via the class-level DSL, and implement #use(context:, **input).
Registration is automatic: every Tool subclass is discoverable via Tool.registered
(resolved from Tool.descendants, so it survives Zeitwerk reloads). Availability is not — the
Harness builds the LLM-visible catalog from the agent's attached toolkits
(+Agent#available_tools_for+, fail-closed), so a registered tool reaches the model only through a
toolkit that contains it. Tools have no protege_id DSL — the id is derived from the class
basename with the Tool suffix stripped, then snake_cased (+SendEmailTool+ → :send_email,
HTTPFetchTool → :http_fetch).
Tools must return a Protege::Result from #use — construct one via Result.success(**data) /
Result.failure(reason:, **data) (or the +#success+/+#failure+ helpers). The Harness serializes
the result to JSON for the LLM's tool message, so a failed result lets the model self-correct.
Contract
def self.id; end # Symbol derived from class basename
def self.description; end # String declared via the DSL
def self.input_schema; end # Hash declared via the DSL (JSON Schema)
def use(context:, **input); end # Context + parsed input → Protege::Result
Defined Under Namespace
Modules: ClassMethods
Class Method Summary collapse
-
.find!(name) ⇒ Object
Find a tool instance by its string id.
-
.included(base) ⇒ void
Extend the includer with
ClassMethodsso the class-level DSL becomes available. -
.invoke(tool_call, context:) ⇒ Protege::ToolResult
Look up a tool by name, execute it, and pair the call with its outcome.
-
.registered ⇒ Array<Class>
List all concrete tool classes.
Instance Method Summary collapse
-
#failure(reason:, **data) ⇒ Protege::Result
Build a failed
Protege::Result. -
#success(**data) ⇒ Protege::Result
Build a successful
Protege::Result. -
#use(context:, **_input) ⇒ Protege::Result
Execute the tool's work.
Class Method Details
.find!(name) ⇒ Object
Find a tool instance by its string id.
69 70 71 72 73 74 |
# File 'lib/protege/extensions/tool_mixin.rb', line 69 def find!(name) klass = registered.find { _1.id.to_s == name } raise Protege::ToolNotFoundError.new(name:) unless klass klass.new end |
.included(base) ⇒ void
This method returns an undefined value.
Extend the includer with ClassMethods so the class-level DSL becomes available.
50 51 52 |
# File 'lib/protege/extensions/tool_mixin.rb', line 50 def included(base) base.extend(ClassMethods) end |
.invoke(tool_call, context:) ⇒ Protege::ToolResult
Look up a tool by name, execute it, and pair the call with its outcome.
Both failure modes resolve to a failed Result so the caller (the Harness) never sees a
raw error — the model gets a serialized failure it can self-correct from:
- an exception raised inside
#use(or byfind!) is caught, and - a
#usethat returns a non-+Result+ violates the tool contract and is reported asProtege::InvalidToolResultError, rather than blowing up later when theHarnesscalls#success?on the bad value.
Enforcement of tool scoping lives here: a call to any tool outside the agent's effective scope
for this run (the tools its attached toolkits expose to the run's sender, read from
context.agent) is refused before #use runs. Advertising a narrowed catalogue in the request
isn't enough — a tool that left scope mid-thread stays visible to the model in earlier turns and
could still be called — so dispatch is the backstop.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/protege/extensions/tool_mixin.rb', line 95 def invoke(tool_call, context:) if context.agent.available_tool_ids_for(context:).exclude?(tool_call.name.to_sym) return failed_tool_result(tool_call:, error: Protege::ToolNotAvailableError.new(name: tool_call.name)) end tool = find!(tool_call.name) input = (tool_call.input || {}).symbolize_keys result = tool.use(context:, **input) return Protege::ToolResult.new(tool_call:, result:) if result.is_a?(Result) failed_tool_result(tool_call:, error: Protege::InvalidToolResultError.new(tool: tool.class, got: result.class)) rescue StandardError => e failed_tool_result(tool_call:, error: e) end |
.registered ⇒ Array<Class>
List all concrete tool classes.
Resolved via Tool.descendants so the registry survives Zeitwerk reloads without manual
bookkeeping. Anonymous classes (nil name) are excluded.
60 61 62 |
# File 'lib/protege/extensions/tool_mixin.rb', line 60 def registered Tool.descendants.reject { _1.name.nil? } end |
Instance Method Details
#failure(reason:, **data) ⇒ Protege::Result
Build a failed Protege::Result.
Call from #use to signal the tool could not complete its work. reason may be a String or an
Exception; strings are wrapped in Protege::GenericError. data is merged into the payload.
171 172 173 |
# File 'lib/protege/extensions/tool_mixin.rb', line 171 def failure(reason:, **data) Result.failure(reason:, **data) end |
#success(**data) ⇒ Protege::Result
Build a successful Protege::Result.
Call from #use to signal the tool completed its work. data becomes the payload the LLM sees
in the tool-result message.
155 156 157 |
# File 'lib/protege/extensions/tool_mixin.rb', line 155 def success(**data) Result.success(**data) end |
#use(context:, **_input) ⇒ Protege::Result
Execute the tool's work. Concrete tools must override.
140 141 142 143 |
# File 'lib/protege/extensions/tool_mixin.rb', line 140 def use(context:, **_input) raise NotImplementedError, "#{self.class} must implement #use(context:, **input) -> Protege::Result" end |