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

class OrderSupportAgent < LittleGhost::Agent
tools OrderStatusTool
end

OrderSupportAgent.ask(
"Where is order 481?",
actor_id: authenticated_user.id,
context: {account_id: authenticated_user.}
)

Each value comes from a different part of the run:

[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 a normalized internal result. 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.

See the Tools guide for the complete path from model-selected input to application context, sandbox delegation, concurrency, and code mode.

Defined Under Namespace

Classes: Binding, ExecutionResult, Result, SchemaValidator

Constant Summary collapse

UNSET_RESULT_VALUE =

:nodoc:

Object.new.freeze

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.



363
364
365
366
# File 'lib/little_ghost/tool.rb', line 363

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

Instance Attribute Details

#contextObject

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



349
350
351
# File 'lib/little_ghost/tool.rb', line 349

def context
  @context
end

Class Method Details

.available?(binding) ⇒ Boolean

Returns whether this Tool should be registered for binding.

Returns:

  • (Boolean)


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

def available?(binding)
  !availability_value || !!availability_value.call(binding)
end

.available_if(&predicate) ⇒ Object

Declares whether this Tool is available for a run-scoped binding. With no block, returns the configured predicate or nil. ToolRegistry omits a Tool whose predicate returns false before constructing it.



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

def available_if(&predicate)
  return availability_value unless predicate

  self.availability_value = predicate
end

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


291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/little_ghost/tool.rb', line 291

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.



236
237
238
239
240
# File 'lib/little_ghost/tool.rb', line 236

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.



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

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)


250
251
252
253
254
255
256
257
# File 'lib/little_ghost/tool.rb', line 250

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.



313
314
315
316
317
318
319
# File 'lib/little_ghost/tool.rb', line 313

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.



225
226
227
228
229
# File 'lib/little_ghost/tool.rb', line 225

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.



369
370
# File 'lib/little_ghost/tool.rb', line 369

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.



415
416
417
# File 'lib/little_ghost/tool.rb', line 415

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

#closeObject

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



420
421
# File 'lib/little_ghost/tool.rb', line 420

def close
end

#descriptionObject

Model-visible description declared by the tool class.



354
355
# File 'lib/little_ghost/tool.rb', line 354

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)


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

def exclusive? = self.class.exclusive

#execute(input, context: RunContext.new) ⇒ Object

Validates input and invokes the Tool, returning its normalized outcome.

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.



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/little_ghost/tool.rb', line 386

def execute(input, context: RunContext.new)
  context ||= RunContext.new
  errors = if self.class.validate_input_schema_value
    SchemaValidator.new(self.class.input_schema).validate(input)
  else
    []
  end
  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)
  return success(value.value, artifacts: value.artifacts) if value.is_a?(Result)

  success(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.



356
357
# File 'lib/little_ghost/tool.rb', line 356

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

#modelObject

Bound model, when available.



375
376
# File 'lib/little_ghost/tool.rb', line 375

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

#runObject

Bound run, when available.



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

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

#runtimeObject

Bound runtime, when available.



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

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

#sandboxObject

Bound sandbox, when available.



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

def sandbox = binding.sandbox

#specificationObject

Frozen provider-facing tool specification.



358
359
# File 'lib/little_ghost/tool.rb', line 358

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.



352
353
# File 'lib/little_ghost/tool.rb', line 352

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

#workspaceObject

Bound workspace, when available.



377
378
# File 'lib/little_ghost/tool.rb', line 377

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