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

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

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.

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.



320
321
322
323
# File 'lib/little_ghost/tool.rb', line 320

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

Instance Attribute Details

#contextObject

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

Raises:

  • (ArgumentError)


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.

Raises:

  • (ArgumentError)


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

.specificationObject

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

#agentObject

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

#closeObject

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



372
373
# File 'lib/little_ghost/tool.rb', line 372

def close
end

#descriptionObject

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.

Returns:

  • (Boolean)


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?
    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.



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

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

#modelObject

Bound model, when available.



332
333
# File 'lib/little_ghost/tool.rb', line 332

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

#runObject

Bound run, when available.



328
329
# File 'lib/little_ghost/tool.rb', line 328

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

#runtimeObject

Bound runtime, when available.



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

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

#sandboxObject

Bound sandbox, when available.



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

def sandbox = binding.sandbox

#specificationObject

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_nameObject

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.

#workspaceObject

Bound workspace, when available.



334
335
# File 'lib/little_ghost/tool.rb', line 334

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