Class: Smith::Agent
- Inherits:
-
RubyLLM::Agent
- Object
- RubyLLM::Agent
- Smith::Agent
- 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 :namecalls 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
-
.model_block ⇒ Object
readonly
Returns the value of attribute model_block.
Class Method Summary collapse
- .budget(**opts) ⇒ Object
-
.chat(**kwargs) ⇒ Object
Closes the
inputscontract at the chat() boundary AND runs the Smith::Models::Normalizer. - .data_volume(value = nil) ⇒ Object
- .execution_identity(value = EXECUTION_IDENTITY_UNSET) ⇒ Object
- .fallback_models(*models) ⇒ Object
- .guardrails(klass = nil) ⇒ Object
- .headers(**headers_kwargs, &block) ⇒ Object
- .inherited(subclass) ⇒ Object
-
.inputs(*names) ⇒ Object
MERGING override: getter always returns user-declared ∪ reserved; setter validates user names against reserved + stores only user names.
- .instructions(text = nil, **prompt_locals, &block) ⇒ Object
-
.model(model_id = nil, **options, &block) ⇒ Object
Extends RubyLLM::Agent.model with a block-form for context-driven resolution at chat-construction time.
-
.model_configured? ⇒ Boolean
Whether this agent class has any model configured (static or block).
- .output_schema(klass = nil) ⇒ Object
- .params(**params_kwargs, &block) ⇒ Object
- .publish_registration! ⇒ Object
- .register_as(name = nil, publish: true) ⇒ Object
- .schema(value = nil, &block) ⇒ Object
-
.tools(*tools, &block) ⇒ Object
Normalizes the |ctx| DSL across RubyLLM's block-form attribute setters.
Class Attribute Details
.model_block ⇒ Object (readonly)
Returns the value of attribute model_block.
132 133 134 |
# File 'lib/smith/agent.rb', line 132 def model_block @model_block end |
Class Method Details
.budget(**opts) ⇒ Object
39 40 41 42 43 |
# File 'lib/smith/agent.rb', line 39 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!.
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/smith/agent.rb', line 187 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 = super Smith::Models::Normalizer.apply!(llm_chat, profile: profile) if profile llm_chat end |
.data_volume(value = nil) ⇒ Object
57 58 59 60 61 |
# File 'lib/smith/agent.rb', line 57 def data_volume(value = nil) return @data_volume if value.nil? @data_volume = value end |
.execution_identity(value = EXECUTION_IDENTITY_UNSET) ⇒ Object
30 31 32 33 34 35 36 37 |
# File 'lib/smith/agent.rb', line 30 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
63 64 65 66 67 68 69 70 |
# File 'lib/smith/agent.rb', line 63 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 |
.guardrails(klass = nil) ⇒ Object
45 46 47 48 49 |
# File 'lib/smith/agent.rb', line 45 def guardrails(klass = nil) return @guardrails_class if klass.nil? @guardrails_class = klass end |
.headers(**headers_kwargs, &block) ⇒ Object
244 245 246 247 248 |
# File 'lib/smith/agent.rb', line 244 def headers(**headers_kwargs, &block) return super unless block super(&wrap_runtime_block(block)) end |
.inherited(subclass) ⇒ Object
18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/smith/agent.rb', line 18 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.
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/smith/agent.rb', line 159 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
232 233 234 235 236 |
# File 'lib/smith/agent.rb', line 232 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.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/smith/agent.rb', line 109 def model(model_id = nil, **, &block) if block raise ArgumentError, "model can take a string id OR a block, not both" if model_id || !.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.
150 151 152 |
# File 'lib/smith/agent.rb', line 150 def model_configured? !chat_kwargs[:model].nil? || !@model_block.nil? end |
.output_schema(klass = nil) ⇒ Object
51 52 53 54 55 |
# File 'lib/smith/agent.rb', line 51 def output_schema(klass = nil) return @output_schema_class if klass.nil? @output_schema_class = klass end |
.params(**params_kwargs, &block) ⇒ Object
238 239 240 241 242 |
# File 'lib/smith/agent.rb', line 238 def params(**params_kwargs, &block) return super unless block super(&wrap_runtime_block(block)) end |
.publish_registration! ⇒ Object
81 82 83 84 85 86 |
# File 'lib/smith/agent.rb', line 81 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
72 73 74 75 76 77 78 79 |
# File 'lib/smith/agent.rb', line 72 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
250 251 252 253 254 |
# File 'lib/smith/agent.rb', line 250 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.
226 227 228 229 230 |
# File 'lib/smith/agent.rb', line 226 def tools(*tools, &block) return super unless block super(&wrap_runtime_block(block)) end |