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), and the Harness
enumerates that list to build the LLM-visible tool catalog. 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.
67 68 69 70 71 72 |
# File 'lib/protege/extensions/tool_mixin.rb', line 67 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.
48 49 50 |
# File 'lib/protege/extensions/tool_mixin.rb', line 48 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 persona's effective scope
(its code grant minus disabled_tool_ids, read from context.persona) is refused before #use
runs. Advertising a narrowed catalogue in the request isn't enough — a tool disabled mid-thread
stays visible to the model in earlier turns and could still be called — so dispatch is the
backstop.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/protege/extensions/tool_mixin.rb', line 93 def invoke(tool_call, context:) if context.persona.available_tool_ids.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.
58 59 60 |
# File 'lib/protege/extensions/tool_mixin.rb', line 58 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.
169 170 171 |
# File 'lib/protege/extensions/tool_mixin.rb', line 169 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.
153 154 155 |
# File 'lib/protege/extensions/tool_mixin.rb', line 153 def success(**data) Result.success(**data) end |
#use(context:, **_input) ⇒ Protege::Result
Execute the tool's work. Concrete tools must override.
138 139 140 141 |
# File 'lib/protege/extensions/tool_mixin.rb', line 138 def use(context:, **_input) raise NotImplementedError, "#{self.class} must implement #use(context:, **input) -> Protege::Result" end |