Class: Mistri::Tool

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/tool.rb

Overview

A tool the agent can call: a name, a description, a JSON Schema for its arguments, and a handler. Model calls reach the handler as canonical argument hashes, optionally normalized by the tool before policy sees them. Trusted direct calls keep ordinary Ruby Hash semantics.

Constant Summary collapse

EMPTY_SCHEMA =

A no-argument tool still needs a valid object schema; providers reject a bare empty hash.

{
  type: "object", properties: {}.freeze, required: [].freeze,
  additionalProperties: false
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, description:, input_schema: EMPTY_SCHEMA, eager_input_streaming: false, needs_approval: false, ends_turn: false, timeout: nil, argument_normalizer: nil, argument_validator: nil, complete_argument_validator: nil, &handler) ⇒ Tool

Returns a new instance of Tool.

Raises:

  • (ArgumentError)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/mistri/tool.rb', line 35

def initialize(name:, description:, input_schema: EMPTY_SCHEMA, eager_input_streaming: false,
               needs_approval: false, ends_turn: false, timeout: nil,
               argument_normalizer: nil, argument_validator: nil,
               complete_argument_validator: nil, &handler)
  raise ArgumentError, "tool #{name.inspect} needs a handler block" unless handler
  unless argument_normalizer.nil? || argument_normalizer.respond_to?(:call)
    raise ArgumentError, "argument_normalizer must be callable"
  end
  unless argument_validator.nil? || argument_validator.respond_to?(:call)
    raise ArgumentError, "argument_validator must be callable"
  end
  unless complete_argument_validator.nil? || complete_argument_validator.respond_to?(:call)
    raise ArgumentError, "complete_argument_validator must be callable"
  end
  if argument_validator && complete_argument_validator
    raise ArgumentError,
          "choose argument_validator or complete_argument_validator, not both"
  end

  @name = name.to_s
  @description = description
  @schema_validator = Schema.tool_validator(
    input_schema, complete: !complete_argument_validator.nil?
  )
  @input_schema = @schema_validator.schema
  @eager_input_streaming = eager_input_streaming
  @needs_approval = needs_approval
  @ends_turn = ends_turn
  @timeout = timeout
  @argument_normalizer = argument_normalizer
  @argument_validator = argument_validator
  @complete_argument_validator = complete_argument_validator
  @handler = handler
end

Instance Attribute Details

#descriptionObject (readonly)

Returns the value of attribute description.



18
19
20
# File 'lib/mistri/tool.rb', line 18

def description
  @description
end

#input_schemaObject (readonly)

Returns the value of attribute input_schema.



18
19
20
# File 'lib/mistri/tool.rb', line 18

def input_schema
  @input_schema
end

#nameObject (readonly)

Returns the value of attribute name.



18
19
20
# File 'lib/mistri/tool.rb', line 18

def name
  @name
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



18
19
20
# File 'lib/mistri/tool.rb', line 18

def timeout
  @timeout
end

Class Method Details

.define(name, description, input_schema: nil, schema: nil, &handler) ⇒ Object

Define a tool. Give the argument shape as a JSON Schema value via input_schema:, or build it in Ruby with a schema: block. The result is canonicalized and owned once so provider and local semantics cannot drift.

Tool.define("get_weather", "Weather for a city",
          schema: -> { string :city, "City name", required: true }) do |args|
Weather.for(args["city"])
end

Raises:

  • (ArgumentError)


28
29
30
31
32
33
# File 'lib/mistri/tool.rb', line 28

def self.define(name, description, input_schema: nil, schema: nil, **, &handler)
  raise ArgumentError, "choose input_schema or schema, not both" if schema && !input_schema.nil?

  input_schema = schema ? Schema.build(&schema) : EMPTY_SCHEMA if input_schema.nil?
  new(name: name, description: description, input_schema: input_schema, **, &handler)
end

Instance Method Details

#argument_violations(arguments) ⇒ Object

Core validation is always authoritative for its portable subset. A supplemental validator adds domain rules; an explicitly complete one additionally owns schema interactions core cannot represent.



104
105
106
# File 'lib/mistri/tool.rb', line 104

def argument_violations(arguments)
  validate_arguments(arguments, owned: false)
end

#call(arguments, context = ToolContext.new) ⇒ Object

A handler may return a ToolResult to add host-only UI or declare a model-readable failure. Its ui payload is canonicalized through JSON here so the live event and a reloaded session read the identical shape.

Handlers receive (arguments, context). A proc that declares one parameter ignores the context invisibly; a lambda opts in by arity. Direct calls are trusted host invocations: they apply the compatibility normalizer, but do not canonicalize or validate as model calls do. The executor marks arguments the Agent already prepared so subclasses keep the historical #call extension point without running normalization twice.



80
81
82
83
84
85
86
87
# File 'lib/mistri/tool.rb', line 80

def call(arguments, context = ToolContext.new)
  arguments = normalize_arguments(arguments || {}) unless prepared_context?(context)
  result = invoke(arguments || {}, context)
  return serialize_result(result) unless result.is_a?(ToolResult)

  result.with(content: serialize_result(result.content),
              ui: result.ui && JSON.parse(JSON.generate(result.ui)))
end

#ends_turn?Boolean

A tool that is the last word of its turn: once it executes, the loop ends the run instead of prompting the model again. This is how a tool like ask_user hands the floor to a human structurally, with no prompt discipline required; the answer arrives as the next run's input.

Returns:

  • (Boolean)


125
126
127
# File 'lib/mistri/tool.rb', line 125

def ends_turn?
  @ends_turn
end

#needs_approval?(arguments) ⇒ Boolean

Whether this call should pause for a human. true/false, or a callable given the parsed arguments so a tool can gate only the risky calls (needs_approval: ->(args) { args.to_i > 100 }).

Returns:

  • (Boolean)


117
118
119
# File 'lib/mistri/tool.rb', line 117

def needs_approval?(arguments)
  @needs_approval.respond_to?(:call) ? @needs_approval.call(arguments) : @needs_approval
end

#normalize_arguments(arguments) ⇒ Object

Normalization is an explicit per-tool migration boundary, never a global coercion policy. Agent calls this once before policy; direct trusted invocations get the same compatibility behavior through #call.

Raises:

  • (ArgumentError)


92
93
94
95
96
97
98
99
# File 'lib/mistri/tool.rb', line 92

def normalize_arguments(arguments)
  return arguments unless @argument_normalizer

  normalized = @argument_normalizer.call(arguments)
  raise ArgumentError, "argument_normalizer must return a Hash" unless normalized.is_a?(Hash)

  normalized
end

#prepared_argument_violations(arguments) ⇒ Object

Agent has already moved the value through ToolCall's ownership boundary, so its hot path can avoid copying the same immutable JSON twice.



110
111
112
# File 'lib/mistri/tool.rb', line 110

def prepared_argument_violations(arguments)
  validate_arguments(arguments, owned: true)
end

#specObject

The provider-facing definition; every serializer accepts this shape.



130
131
132
133
134
# File 'lib/mistri/tool.rb', line 130

def spec
  definition = { name: @name, description: @description, input_schema: @input_schema }
  definition[:eager_input_streaming] = true if @eager_input_streaming
  definition
end