Class: LittleGhost::Tool

Inherits:
Object
  • Object
show all
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

result = TicketStatusTool.new.execute("ticket_id" => "SUP-481")
result.success?            # => true
JSON.parse(result.content) # => {"ticket_id"=>"SUP-481", "status"=>"waiting_on_customer"}

The class DSL produces the frozen specification sent to models. execute validates incoming arguments, invokes call, and normalizes strings, JSON-compatible collections, and other return values to model-facing text. Tool.define offers the same contract for an embedded implementation.

A tool registry creates one instance per agent run and supplies a Binding for access to the agent, run, runtime, model, workspace, and sandbox. Mutable per-instance state therefore belongs to that run. Registries close tools that implement close; exclusive true serializes calls against every other exclusive tool 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.

Defined Under Namespace

Classes: Binding, ExecutionResult, SchemaValidator

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::ClassAttributes

class_attribute, included

Constructor Details

#initialize(binding: Binding.new) ⇒ Tool

Creates a tool with the run-scoped collaborators in binding.



264
265
266
267
# File 'lib/little_ghost/tool.rb', line 264

def initialize(binding: Binding.new)
  @binding = binding
  @state = {}
end

Instance Attribute Details

#contextObject

RunContext supplied to the current #execute call, or nil outside execution.



250
251
252
# File 'lib/little_ghost/tool.rb', line 250

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") }

Raises:

  • (ArgumentError)


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/little_ghost/tool.rb', line 192

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.



151
152
153
154
155
# File 'lib/little_ghost/tool.rb', line 151

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.



179
180
181
182
183
# File 'lib/little_ghost/tool.rb', line 179

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.

Raises:

  • (ArgumentError)


165
166
167
168
169
170
171
172
# File 'lib/little_ghost/tool.rb', line 165

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

.specificationObject

The frozen model-facing name, description, and input schema.



214
215
216
217
218
219
220
# File 'lib/little_ghost/tool.rb', line 214

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.



140
141
142
143
144
# File 'lib/little_ghost/tool.rb', line 140

def tool_name(*values)
  return configured_name if values.empty?

  self.tool_name_value = String(values.fetch(0)).freeze
end

Instance Method Details

#agentObject

Bound agent, when the tool belongs to an agent run.



270
271
# File 'lib/little_ghost/tool.rb', line 270

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.



308
309
310
# File 'lib/little_ghost/tool.rb', line 308

def call(_input)
  raise AbstractMethodError, "#{self.class} must implement #call"
end

#closeObject

Releases resources owned by this tool. Subclasses may override it.



313
314
# File 'lib/little_ghost/tool.rb', line 313

def close
end

#descriptionObject

Model-visible description declared by the tool class.



255
256
# File 'lib/little_ghost/tool.rb', line 255

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.

Returns:

  • (Boolean)


261
# File 'lib/little_ghost/tool.rb', line 261

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.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/little_ghost/tool.rb', line 287

def execute(input, context: RunContext.new)
  context ||= RunContext.new
  errors = SchemaValidator.new(self.class.input_schema).validate(input)
  unless errors.empty?
    message = "Invalid tool input: #{errors.join("; ")}"
    return failure(message, error: ToolError.new(message))
  end

  success(sanitize(bound_for(context).call(input)))
rescue CancelledError, DeadlineExceededError, CleanupError
  raise
rescue ToolError => error
  failure(error.message, error:)
rescue => error
  failure("Tool failed (#{error.class})", error:)
end

#input_schemaObject

Normalized JSON input schema declared by the tool class.



257
258
# File 'lib/little_ghost/tool.rb', line 257

def input_schema = self.class.input_schema
# Frozen provider-facing tool specification.

#modelObject

Bound model, when available.



276
277
# File 'lib/little_ghost/tool.rb', line 276

def model = binding.model
# Bound workspace, when available.

#runObject

Bound run, when available.



272
273
# File 'lib/little_ghost/tool.rb', line 272

def run = binding.run
# Bound runtime, when available.

#runtimeObject

Bound runtime, when available.



274
275
# File 'lib/little_ghost/tool.rb', line 274

def runtime = binding.runtime
# Bound model, when available.

#sandboxObject

Bound sandbox, when available.



280
# File 'lib/little_ghost/tool.rb', line 280

def sandbox = binding.sandbox

#specificationObject

Frozen provider-facing tool specification.



259
260
# File 'lib/little_ghost/tool.rb', line 259

def specification = self.class.specification
# Indicates whether calls use the run-wide exclusive-tool lock.

#tool_nameObject

Model-visible name declared by the tool class.



253
254
# File 'lib/little_ghost/tool.rb', line 253

def tool_name = self.class.tool_name
# Model-visible description declared by the tool class.

#workspaceObject

Bound workspace, when available.



278
279
# File 'lib/little_ghost/tool.rb', line 278

def workspace = binding.workspace
# Bound sandbox, when available.