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.



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

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

Instance Attribute Details

#contextObject

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



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

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)


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/little_ghost/tool.rb', line 206

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.



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

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.



193
194
195
196
197
# File 'lib/little_ghost/tool.rb', line 193

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)


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

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.



228
229
230
231
232
233
234
# File 'lib/little_ghost/tool.rb', line 228

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.



154
155
156
157
158
# File 'lib/little_ghost/tool.rb', line 154

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.



284
285
# File 'lib/little_ghost/tool.rb', line 284

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.



325
326
327
# File 'lib/little_ghost/tool.rb', line 325

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

#closeObject

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



330
331
# File 'lib/little_ghost/tool.rb', line 330

def close
end

#descriptionObject

Model-visible description declared by the tool class.



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

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)


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

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.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/little_ghost/tool.rb', line 301

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

  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.message, error:)
rescue => error
  failure("Tool failed (#{error.class})", error:)
end

#input_schemaObject

Normalized JSON input schema declared by the tool class.



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

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

#modelObject

Bound model, when available.



290
291
# File 'lib/little_ghost/tool.rb', line 290

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

#runObject

Bound run, when available.



286
287
# File 'lib/little_ghost/tool.rb', line 286

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

#runtimeObject

Bound runtime, when available.



288
289
# File 'lib/little_ghost/tool.rb', line 288

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

#sandboxObject

Bound sandbox, when available.



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

def sandbox = binding.sandbox

#specificationObject

Frozen provider-facing tool specification.



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

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.



267
268
# File 'lib/little_ghost/tool.rb', line 267

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

#workspaceObject

Bound workspace, when available.



292
293
# File 'lib/little_ghost/tool.rb', line 292

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