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.



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

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

Instance Attribute Details

#contextObject

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



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

def context
  @context
end

Class Method Details

.available?(binding) ⇒ Boolean

Returns whether this Tool should be registered for binding.

Returns:

  • (Boolean)


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

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.



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

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)


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

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.



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

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.



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

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)


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

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.



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

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.



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

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.



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

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.



410
411
412
# File 'lib/little_ghost/tool.rb', line 410

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

#closeObject

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



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

def close
end

#descriptionObject

Model-visible description declared by the tool class.



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

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)


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

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.



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

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



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

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

#modelObject

Bound model, when available.



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

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

#runObject

Bound run, when available.



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

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

#runtimeObject

Bound runtime, when available.



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

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

#sandboxObject

Bound sandbox, when available.



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

def sandbox = binding.sandbox

#specificationObject

Frozen provider-facing tool specification.



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

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.



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

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

#workspaceObject

Bound workspace, when available.



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

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