Class: Smith::Agent

Inherits:
RubyLLM::Agent
  • Object
show all
Defined in:
lib/smith/agent.rb,
lib/smith/agent/registry.rb,
lib/smith/agent/lifecycle.rb,
lib/smith/agent/registry_binding.rb,
lib/smith/agent/registry/introspection.rb,
lib/smith/agent/registry/mutation_boundary.rb,
lib/smith/agent/registry/execution_binding_capture.rb

Defined Under Namespace

Modules: Lifecycle, Registry Classes: RegistryBinding

Constant Summary collapse

RESERVED_INPUT_NAMES =

Reserved input names auto-injected by the normalizer into runtime_context. User-side inputs :name calls cannot redeclare these names; the override raises Smith::AgentError if they try. The getter merges user-declared inputs WITH reserved so subclasses don't lose reserved names when declaring their own.

%i[model_id provider endpoint_mode].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.model_blockObject (readonly)

Returns the value of attribute model_block.



134
135
136
# File 'lib/smith/agent.rb', line 134

def model_block
  @model_block
end

Class Method Details

.budget(**opts) ⇒ Object



41
42
43
44
45
# File 'lib/smith/agent.rb', line 41

def budget(**opts)
  return @budget_config if opts.empty?

  @budget_config = opts
end

.chat(**kwargs) ⇒ Object

Closes the inputs contract at the chat() boundary AND runs the Smith::Models::Normalizer. Hook lives here (not in Lifecycle#attempt_model) so direct callers that construct a chat outside the workflow lifecycle are normalized too. Without this placement, model-specific request translation would only fire for workflow-driven calls.

Single profile lookup: resolved once via Models.find_or_infer and passed through both inject_reserved_inputs and Normalizer.apply!.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/smith/agent.rb', line 189

def chat(**kwargs)
  # Resolve model from explicit kwarg first, then fall back to the
  # class-level chat_kwargs[:model] (set by `model "..."`). The
  # explicit kwarg path fires from Lifecycle#attempt_model (passes
  # the resolved primary or fallback model); the chat_kwargs path
  # fires from direct callers like `Agent.chat` with no args.
  model_id = kwargs[:model] || chat_kwargs[:model]
  profile = resolve_profile(model_id)
  kwargs = inject_reserved_inputs(kwargs, profile)
  kwargs = nil_fill_declared_inputs(kwargs)

  llm_chat = install_tool_execution_context(super)
  Smith::Models::Normalizer.apply!(llm_chat, profile: profile) if profile
  llm_chat
end

.create(**kwargs) ⇒ Object



205
206
207
# File 'lib/smith/agent.rb', line 205

def create(**kwargs)
  install_tool_execution_context(super)
end

.create!(**kwargs) ⇒ Object



209
210
211
# File 'lib/smith/agent.rb', line 209

def create!(**kwargs)
  install_tool_execution_context(super)
end

.data_volume(value = nil) ⇒ Object



59
60
61
62
63
# File 'lib/smith/agent.rb', line 59

def data_volume(value = nil)
  return @data_volume if value.nil?

  @data_volume = value
end

.execution_identity(value = EXECUTION_IDENTITY_UNSET) ⇒ Object



32
33
34
35
36
37
38
39
# File 'lib/smith/agent.rb', line 32

def execution_identity(value = EXECUTION_IDENTITY_UNSET)
  return @execution_identity if value.equal?(EXECUTION_IDENTITY_UNSET)
  unless value.is_a?(String) && /\A[0-9a-f]{64}\z/.match?(value)
    raise ArgumentError, "execution_identity must be a lowercase SHA-256 hex digest"
  end

  @execution_identity = value.dup.freeze
end

.fallback_models(*models) ⇒ Object



65
66
67
68
69
70
71
72
# File 'lib/smith/agent.rb', line 65

def fallback_models(*models)
  return @fallback_models_list if models.empty?

  entries = models.flatten.compact.map(&:to_s)
  raise Smith::WorkflowError, "fallback_models entries must not be blank" if entries.any?(&:empty?)

  @fallback_models_list = entries.uniq
end

.find(id, **kwargs) ⇒ Object



213
214
215
# File 'lib/smith/agent.rb', line 213

def find(id, **kwargs)
  install_tool_execution_context(super)
end

.guardrails(klass = nil) ⇒ Object



47
48
49
50
51
# File 'lib/smith/agent.rb', line 47

def guardrails(klass = nil)
  return @guardrails_class if klass.nil?

  @guardrails_class = klass
end

.headers(**headers_kwargs, &block) ⇒ Object



258
259
260
261
262
# File 'lib/smith/agent.rb', line 258

def headers(**headers_kwargs, &block)
  return super unless block

  super(&wrap_runtime_block(block))
end

.inherited(subclass) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
# File 'lib/smith/agent.rb', line 20

def inherited(subclass)
  super
  subclass.instance_variable_set(:@budget_config, @budget_config)
  subclass.instance_variable_set(:@guardrails_class, @guardrails_class)
  subclass.instance_variable_set(:@output_schema_class, @output_schema_class)
  subclass.instance_variable_set(:@data_volume, @data_volume)
  subclass.instance_variable_set(:@fallback_models_list, @fallback_models_list&.dup)
  subclass.instance_variable_set(:@model_block, @model_block)
  subclass.instance_variable_set(:@execution_identity, nil)
  subclass.instance_variable_set(:@registered_name, nil)
end

.inputs(*names) ⇒ Object

MERGING override: getter always returns user-declared ∪ reserved; setter validates user names against reserved + stores only user names. RubyLLM's bare @input_names = names (agent.rb:96) REPLACES; this override prevents subclasses from losing reserved names when they declare their own inputs.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/smith/agent.rb', line 161

def inputs(*names)
  if names.empty?
    user = @input_names || []
    return (user + RESERVED_INPUT_NAMES).uniq.freeze
  end

  user_names = names.flatten.map(&:to_sym)
  collisions = user_names & RESERVED_INPUT_NAMES
  if collisions.any?
    raise Smith::AgentError,
          "agent input names #{collisions.inspect} are reserved by Smith. " \
          "Reserved names #{RESERVED_INPUT_NAMES.inspect} are auto-injected by " \
          "Smith::Models::Normalizer into runtime_context. " \
          "Rename your inputs to avoid the collision."
  end

  @input_names = user_names.freeze
end

.instructions(text = nil, **prompt_locals, &block) ⇒ Object



246
247
248
249
250
# File 'lib/smith/agent.rb', line 246

def instructions(text = nil, **prompt_locals, &block)
  return super unless block

  super(text, **prompt_locals, &wrap_runtime_block(block))
end

.model(model_id = nil, **options, &block) ⇒ Object

Extends RubyLLM::Agent.model with a block-form for context-driven resolution at chat-construction time.

Static form model "gpt-5-mini":

Stores into @chat_kwargs[:model] via RubyLLM's existing path.
Model id is fixed at class-load time.

Block form model { |context| ... }:

Stores the block as @model_block. Smith's lifecycle resolves it
at chat-construction time using the workflow's @context (Hash).
Return value must be a non-empty string; non-string / empty / nil
returns surface as Smith::AgentError at the resolution point
(see Smith::Agent::Lifecycle#build_model_chain).

Mutually exclusive within a single declaration: passing both a string id and a block raises ArgumentError. Redeclaring with the other form clears the previous setting (static replaces block, block replaces static).

Composes with fallback_models: resolved primary, then declared fallbacks, in order. Same path as static-form fallback.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/smith/agent.rb', line 111

def model(model_id = nil, **options, &block)
  if block
    raise ArgumentError, "model can take a string id OR a block, not both" if model_id || !options.empty?

    @model_block = block
    # Clear any stale `@chat_kwargs[:model]` from a prior static-form
    # declaration. Smith's workflow lifecycle resolves block-form
    # correctly via `build_model_chain` (which checks @model_block
    # first), but RubyLLM's direct `chat()` and `with_rails_chat_record`
    # paths splat `**chat_kwargs` to the constructor; without this
    # delete, those paths would silently use the stale static id.
    # This is the only place Smith mutates a RubyLLM-owned ivar; the
    # mutation is well-scoped (only :model, only on block-form
    # declaration) and matches RubyLLM's own pattern of dup'ing
    # @chat_kwargs through its `inherited` hook.
    @chat_kwargs ||= {}
    @chat_kwargs.delete(:model)
  else
    @model_block = nil
    super
  end
end

.model_configured?Boolean

Whether this agent class has any model configured (static or block). Smith::Workflow::Execution uses this as a precondition for invoking the agent; agents declared without a model are skipped.

Returns:

  • (Boolean)


152
153
154
# File 'lib/smith/agent.rb', line 152

def model_configured?
  !chat_kwargs[:model].nil? || !@model_block.nil?
end

.output_schema(klass = nil) ⇒ Object



53
54
55
56
57
# File 'lib/smith/agent.rb', line 53

def output_schema(klass = nil)
  return @output_schema_class if klass.nil?

  @output_schema_class = klass
end

.params(**params_kwargs, &block) ⇒ Object



252
253
254
255
256
# File 'lib/smith/agent.rb', line 252

def params(**params_kwargs, &block)
  return super unless block

  super(&wrap_runtime_block(block))
end

.publish_registration!Object



83
84
85
86
87
88
# File 'lib/smith/agent.rb', line 83

def publish_registration!
  name = @registered_name
  raise Smith::AgentRegistryError, "agent registration identity is not configured" unless name

  Registry.ensure_registered(name.to_sym, self)
end

.register_as(name = nil, publish: true) ⇒ Object

Raises:

  • (ArgumentError)


74
75
76
77
78
79
80
81
# File 'lib/smith/agent.rb', line 74

def register_as(name = nil, publish: true)
  return @registered_name if name.nil?

  raise ArgumentError, "publish must be true or false" unless [true, false].include?(publish)

  @registered_name = canonical_registration_name(name)
  publish ? publish_registration! : self
end

.schema(value = nil, &block) ⇒ Object



264
265
266
267
268
# File 'lib/smith/agent.rb', line 264

def schema(value = nil, &block)
  return super unless block

  super(&wrap_runtime_block(block))
end

.tools(*tools, &block) ⇒ Object

Normalizes the |ctx| DSL across RubyLLM's block-form attribute setters.

RubyLLM evaluates these blocks via runtime.instance_exec(&block), which sets self to the runtime_context but passes NO positional arguments, so tools do |ctx| ctx.form_kind end would silently bind ctx = nil and crash on the first method call. Smith's model block-form already uses block.call(@context) (an explicit Hash arg), giving agent authors a uniform |ctx| mental model. These overrides carry that convention through to RubyLLM's setters by wrapping any block so |ctx| receives the runtime_context AND self is still the runtime (preserving RubyLLM's bare-method-dispatch convention for zero-arity blocks).

Behavior matrix:

tools do            ... end  (arity 0): preserved as-is; bare method
                                       calls dispatch to runtime via
                                       instance_exec (RubyLLM idiom)
tools do |ctx|      ... end  (arity 1): wrapped; ctx receives runtime
                                       AND self is runtime, so both
                                       `ctx.x` and bare `x` work

Lambdas with arity 0 are preserved as-is (strict-arity safe). The wrapping path uses Proc semantics, so extra args don't raise.



240
241
242
243
244
# File 'lib/smith/agent.rb', line 240

def tools(*tools, &block)
  return super unless block

  super(&wrap_runtime_block(block))
end