Class: LittleGhost::Tool
- Inherits:
-
Object
- Object
- LittleGhost::Tool
- Extended by:
- Support::ClassAttributes
- Defined in:
- lib/little_ghost/tool.rb
Overview
Give an agent a validated way to call application code. Every tool declares a model-visible name, description, and input shape before implementing its operation.
class TicketStatusTool < LittleGhost::Tool
tool_name "ticket_status"
description "Look up a support ticket's status."
input_schema type: "object", properties: {
ticket_id: {type: "string"}
}, required: ["ticket_id"], additionalProperties: false
def call(input)
{ticket_id: input.fetch("ticket_id"), status: "waiting_on_customer"}
end
end
class CustomerSupportAgent < LittleGhost::Agent
tools TicketStatusTool
end
run = CustomerSupportAgent.ask("What is happening with ticket SUP-481?")
run.response
Use application context for authorization, never model-selected input:
class OrderStatusTool < LittleGhost::Tool
description "Look up an order for the current account."
input_schema type: "object", properties: {
order_number: {type: "string"}
}, required: ["order_number"], additionalProperties: false
def call(input)
Orders.status_for(
actor_id: run.invocation.actor_id,
account_id: run.invocation.context.fetch("account_id"),
order_number: input.fetch("order_number")
)
end
end
OrderSupportAgent.ask(
"Where is order 481?",
actor_id: authenticated_user.id,
context: {account_id: authenticated_user.account_id}
)
These values cross different trust boundaries:
[input] Arguments selected by the model. The schema checks their shape, not their permission to perform an operation. [run.invocation.context] Current request values supplied by the application. Use these for authorization after the application authenticates the caller. [context.state] Mutable working state for the run. It may include values restored from a Session, so check saved values again before trusting them. [Tool::Binding] Run-scoped objects such as the Agent, Run, Runtime, workspace, and sandbox. The Binding supplies #run; it does not contain model arguments.
The class DSL produces the specification sent to models. During an Agent
run, the tool registry creates and binds one Tool instance. Tests and custom
integrations may call execute directly; it validates the arguments, calls
call, and returns an ExecutionResult. Tool.define offers the same contract
for an embedded implementation.
Mutable Tool instance state belongs to one Agent run. Registries close tool
instances that implement close; exclusive true prevents that tool
from overlapping other exclusive tools in the same run.
Validation and application ToolError failures become error results. A ToolError message is visible to the model and must be safe to disclose; unexpected exception messages are replaced with their class name. Cancellation, deadlines, and cleanup errors propagate instead of becoming ordinary tool output. The configured sandbox, not Tool itself, enforces filesystem and process isolation.
Direct Known Subclasses
LittleGhost::Tools::Filesystem::ListFiles, LittleGhost::Tools::Filesystem::ReadFile, LittleGhost::Tools::Filesystem::ReplaceInFile, LittleGhost::Tools::Filesystem::WriteFile, LittleGhost::Tools::Shell, LittleGhost::Tools::WriteTodos
Defined Under Namespace
Classes: Binding, ExecutionResult, SchemaValidator
Instance Attribute Summary collapse
-
#context ⇒ Object
readonly
RunContext supplied to the current #execute call, or nil outside execution.
Class Method Summary collapse
-
.define(name:, description:, input_schema: {}, &implementation) ⇒ Object
Creates an anonymous Tool subclass backed by
implementation. -
.description(*values) ⇒ Object
:call-seq: description() -> String description(value) -> value.
-
.exclusive(*values) ⇒ Object
:call-seq: exclusive() -> true or false exclusive(value) -> value.
-
.input_schema(*values) ⇒ Object
:call-seq: input_schema() -> Hash input_schema(schema) -> schema.
-
.specification ⇒ Object
The frozen model-facing name, description, and input schema.
-
.tool_name(*values) ⇒ Object
:call-seq: tool_name() -> String tool_name(value) -> value.
Instance Method Summary collapse
-
#agent ⇒ Object
Bound agent, when the tool belongs to an agent run.
-
#call(_input) ⇒ Object
Implements the model-requested operation.
-
#close ⇒ Object
Releases resources owned by this tool.
-
#description ⇒ Object
Model-visible description declared by the tool class.
-
#exclusive? ⇒ Boolean
Indicates whether calls use the run-wide exclusive-tool lock.
-
#execute(input, context: RunContext.new) ⇒ Object
Validates
inputand invokes the tool, returning an ExecutionResult. -
#initialize(binding: Binding.new) ⇒ Tool
constructor
Creates a tool with the run-scoped collaborators in
binding. -
#input_schema ⇒ Object
Normalized JSON input schema declared by the tool class.
-
#model ⇒ Object
Bound model, when available.
-
#run ⇒ Object
Bound run, when available.
-
#runtime ⇒ Object
Bound runtime, when available.
-
#sandbox ⇒ Object
Bound sandbox, when available.
-
#specification ⇒ Object
Frozen provider-facing tool specification.
-
#tool_name ⇒ Object
Model-visible name declared by the tool class.
-
#workspace ⇒ Object
Bound workspace, when available.
Methods included from Support::ClassAttributes
Constructor Details
Instance Attribute Details
#context ⇒ Object
RunContext supplied to the current #execute call, or nil outside execution.
306 307 308 |
# File 'lib/little_ghost/tool.rb', line 306 def context @context end |
Class Method Details
.define(name:, description:, input_schema: {}, &implementation) ⇒ Object
Creates an anonymous Tool subclass backed by implementation.
The block receives input and may also accept the context: keyword.
tool = LittleGhost::Tool.define(
name: "echo", description: "Echo text.",
input_schema: {type: "object"}
) { |input| input.fetch("text") }
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/little_ghost/tool.rb', line 248 def define(name:, description:, input_schema: {}, &implementation) raise ArgumentError, "A tool implementation block is required" unless implementation Class.new(self) do tool_name(name) description(description) input_schema(input_schema) define_method(:call) do |input| accepts_context = implementation.parameters.any? do |kind, parameter| kind == :keyrest || (%i[key keyreq].include?(kind) && parameter == :context) end if accepts_context implementation.call(input, context: context) else implementation.call(input) end end end end |
.description(*values) ⇒ Object
:call-seq:
description() -> String
description(value) -> value
The model-visible description used to decide when the tool applies.
207 208 209 210 211 |
# File 'lib/little_ghost/tool.rb', line 207 def description(*values) return description_value if values.empty? self.description_value = String(values.fetch(0)).freeze end |
.exclusive(*values) ⇒ Object
:call-seq:
exclusive() -> true or false
exclusive(value) -> value
Whether calls acquire the run-wide exclusive tool lock.
235 236 237 238 239 |
# File 'lib/little_ghost/tool.rb', line 235 def exclusive(*values) return !!exclusive_value if values.empty? self.exclusive_value = !!values.fetch(0) end |
.input_schema(*values) ⇒ Object
:call-seq:
input_schema() -> Hash
input_schema(schema) -> schema
The frozen JSON Schema subset used to validate model input.
Setting a non-Hash schema raises ArgumentError. Keys are normalized to strings and the entire value is deeply frozen.
221 222 223 224 225 226 227 228 |
# File 'lib/little_ghost/tool.rb', line 221 def input_schema(*values) return input_schema_value || {}.freeze if values.empty? value = values.fetch(0) raise ArgumentError, "input_schema must be a hash" unless value.is_a?(Hash) self.input_schema_value = deep_freeze(value) end |
.specification ⇒ Object
The frozen model-facing name, description, and input schema.
270 271 272 273 274 275 276 |
# File 'lib/little_ghost/tool.rb', line 270 def specification { name: tool_name, description: description, input_schema: input_schema }.freeze end |
.tool_name(*values) ⇒ Object
:call-seq:
tool_name() -> String
tool_name(value) -> value
The model-visible tool name.
Named classes derive a snake-cased default; passing value replaces it.
196 197 198 199 200 |
# File 'lib/little_ghost/tool.rb', line 196 def tool_name(*values) return configured_name if values.empty? self.tool_name_value = String(values.fetch(0)).freeze end |
Instance Method Details
#agent ⇒ Object
Bound agent, when the tool belongs to an agent run.
326 327 |
# File 'lib/little_ghost/tool.rb', line 326 def agent = binding.agent # Bound run, when available. |
#call(_input) ⇒ Object
Implements the model-requested operation.
Subclasses must override this method. The current RunContext is available
through context while the call executes.
367 368 369 |
# File 'lib/little_ghost/tool.rb', line 367 def call(_input) raise AbstractMethodError, "#{self.class} must implement #call" end |
#close ⇒ Object
Releases resources owned by this tool. Subclasses may override it.
372 373 |
# File 'lib/little_ghost/tool.rb', line 372 def close end |
#description ⇒ Object
Model-visible description declared by the tool class.
311 312 |
# File 'lib/little_ghost/tool.rb', line 311 def description = self.class.description # Normalized JSON input schema declared by the tool class. |
#exclusive? ⇒ Boolean
Indicates whether calls use the run-wide exclusive-tool lock.
317 |
# File 'lib/little_ghost/tool.rb', line 317 def exclusive? = self.class.exclusive |
#execute(input, context: RunContext.new) ⇒ Object
Validates input and invokes the tool, returning an ExecutionResult.
Cancellation, deadline, and cleanup exceptions remain control-flow exceptions. ToolError and unexpected failures become sanitized error results; unexpected exception messages are not exposed to the model.
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/little_ghost/tool.rb', line 343 def execute(input, context: RunContext.new) context ||= RunContext.new errors = SchemaValidator.new(self.class.input_schema).validate(input) unless errors.empty? = "Invalid tool input: #{errors.join("; ")}" return failure(, error: ToolError.new()) end value = bound_for(context).call(input) return normalize_execution_result(value) if value.is_a?(ExecutionResult) success(sanitize(value)) rescue CancelledError, DeadlineExceededError, CleanupError raise rescue ToolError => error failure(error., error:) rescue => error failure("Tool failed (#{error.class})", error:) end |
#input_schema ⇒ Object
Normalized JSON input schema declared by the tool class.
313 314 |
# File 'lib/little_ghost/tool.rb', line 313 def input_schema = self.class.input_schema # Frozen provider-facing tool specification. |
#model ⇒ Object
Bound model, when available.
332 333 |
# File 'lib/little_ghost/tool.rb', line 332 def model = binding.model # Bound workspace, when available. |
#run ⇒ Object
Bound run, when available.
328 329 |
# File 'lib/little_ghost/tool.rb', line 328 def run = binding.run # Bound runtime, when available. |
#runtime ⇒ Object
Bound runtime, when available.
330 331 |
# File 'lib/little_ghost/tool.rb', line 330 def runtime = binding.runtime # Bound model, when available. |
#sandbox ⇒ Object
Bound sandbox, when available.
336 |
# File 'lib/little_ghost/tool.rb', line 336 def sandbox = binding.sandbox |
#specification ⇒ Object
Frozen provider-facing tool specification.
315 316 |
# File 'lib/little_ghost/tool.rb', line 315 def specification = self.class.specification # Indicates whether calls use the run-wide exclusive-tool lock. |
#tool_name ⇒ Object
Model-visible name declared by the tool class.
309 310 |
# File 'lib/little_ghost/tool.rb', line 309 def tool_name = self.class.tool_name # Model-visible description declared by the tool class. |
#workspace ⇒ Object
Bound workspace, when available.
334 335 |
# File 'lib/little_ghost/tool.rb', line 334 def workspace = binding.workspace # Bound sandbox, when available. |