Class: Kernai::Skill
- Inherits:
-
Object
- Object
- Kernai::Skill
- Defined in:
- lib/kernai/skill.rb
Constant Summary collapse
- NO_DEFAULT =
:__no_default__
Instance Attribute Summary collapse
-
#configs ⇒ Object
readonly
Returns the value of attribute configs.
-
#credentials ⇒ Object
readonly
Returns the value of attribute credentials.
-
#description_text ⇒ Object
readonly
Returns the value of attribute description_text.
-
#execute_block ⇒ Object
readonly
Returns the value of attribute execute_block.
-
#inputs ⇒ Object
readonly
Returns the value of attribute inputs.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#produced_kinds ⇒ Object
readonly
Returns the value of attribute produced_kinds.
-
#required_capabilities ⇒ Object
readonly
Returns the value of attribute required_capabilities.
Class Method Summary collapse
- .all ⇒ Object
- .define(name, &block) ⇒ Object
- .find(name) ⇒ Object
- .listing(scope = :all, model: nil) ⇒ Object
- .load_from(pattern) ⇒ Object
- .register(skill) ⇒ Object
- .reload! ⇒ Object
- .reset! ⇒ Object
- .unregister(name) ⇒ Object
Instance Method Summary collapse
-
#call(params = {}) ⇒ Object
Invoke the skill with a positional params hash or keyword args.
-
#call_in_context(params, run_context:) ⇒ Object
Same as #call but forwards a run_context to the SkillContext so the skill’s execute block can reach ‘ctx.run_context` (the host application’s per-run state: current actor, ticket, request id, whatever the host subclassed Kernai::Context to carry).
-
#config(name, type = String, default: nil, description: nil) ⇒ Object
Declare a non-secret configuration value.
-
#credential(name, required: false, description: nil) ⇒ Object
Declare a credential the skill needs.
- #description(text) ⇒ Object
- #execute(&block) ⇒ Object
-
#initialize(name) ⇒ Skill
constructor
A new instance of Skill.
-
#input(name, type, default: NO_DEFAULT, of: nil, schema: nil) ⇒ Object
Declare an input.
-
#produces(*kinds) ⇒ Object
Declare the media kinds the skill may emit back into the conversation (e.g. ‘produces :image` for an image-generation tool).
-
#requires(*caps) ⇒ Object
Declare the model capabilities the skill needs to be runnable.
-
#runnable_on?(model) ⇒ Boolean
A skill is runnable on a given model when that model satisfies every capability declared via ‘requires`.
- #to_description ⇒ Object
Constructor Details
#initialize(name) ⇒ Skill
Returns a new instance of Skill.
80 81 82 83 84 85 86 87 88 89 |
# File 'lib/kernai/skill.rb', line 80 def initialize(name) @name = name.to_sym @inputs = {} @configs = {} @credentials = {} @description_text = nil @execute_block = nil @required_capabilities = [] @produced_kinds = [] end |
Instance Attribute Details
#configs ⇒ Object (readonly)
Returns the value of attribute configs.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def configs @configs end |
#credentials ⇒ Object (readonly)
Returns the value of attribute credentials.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def credentials @credentials end |
#description_text ⇒ Object (readonly)
Returns the value of attribute description_text.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def description_text @description_text end |
#execute_block ⇒ Object (readonly)
Returns the value of attribute execute_block.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def execute_block @execute_block end |
#inputs ⇒ Object (readonly)
Returns the value of attribute inputs.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def inputs @inputs end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def name @name end |
#produced_kinds ⇒ Object (readonly)
Returns the value of attribute produced_kinds.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def produced_kinds @produced_kinds end |
#required_capabilities ⇒ Object (readonly)
Returns the value of attribute required_capabilities.
5 6 7 |
# File 'lib/kernai/skill.rb', line 5 def required_capabilities @required_capabilities end |
Class Method Details
.all ⇒ Object
24 25 26 |
# File 'lib/kernai/skill.rb', line 24 def all @mutex.synchronize { registry.values } end |
.define(name, &block) ⇒ Object
9 10 11 12 13 14 |
# File 'lib/kernai/skill.rb', line 9 def define(name, &block) skill = new(name) skill.instance_eval(&block) register(skill) skill end |
.find(name) ⇒ Object
20 21 22 |
# File 'lib/kernai/skill.rb', line 20 def find(name) @mutex.synchronize { registry[name.to_sym] } end |
.listing(scope = :all, model: nil) ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/kernai/skill.rb', line 28 def listing(scope = :all, model: nil) skills = case scope when nil then [] when :all then all when Array then scope.filter_map { |n| find(n) } else [] end skills = skills.select { |s| Kernai.config.allowed_skills.include?(s.name) } if Kernai.config.allowed_skills skills = skills.select { |s| s.runnable_on?(model) } if model return 'No skills available.' if skills.empty? skills.map(&:to_description).join("\n") end |
.load_from(pattern) ⇒ Object
57 58 59 60 61 62 63 |
# File 'lib/kernai/skill.rb', line 57 def load_from(pattern) @mutex.synchronize do @load_paths ||= [] @load_paths << pattern unless @load_paths.include?(pattern) end Dir.glob(pattern).each { |file| load(file) } end |
.register(skill) ⇒ Object
16 17 18 |
# File 'lib/kernai/skill.rb', line 16 def register(skill) @mutex.synchronize { registry[skill.name] = skill } end |
.reload! ⇒ Object
52 53 54 55 |
# File 'lib/kernai/skill.rb', line 52 def reload! reset! load_paths.each { |path| load_from(path) } end |
.reset! ⇒ Object
48 49 50 |
# File 'lib/kernai/skill.rb', line 48 def reset! @mutex.synchronize { @registry = {} } end |
.unregister(name) ⇒ Object
44 45 46 |
# File 'lib/kernai/skill.rb', line 44 def unregister(name) @mutex.synchronize { registry.delete(name.to_sym) } end |
Instance Method Details
#call(params = {}) ⇒ Object
Invoke the skill with a positional params hash or keyword args. Callers that need to surface the per-run Kernai::Context (e.g. the kernel itself) should use ‘call_in_context` instead.
176 177 178 |
# File 'lib/kernai/skill.rb', line 176 def call(params = {}) call_in_context(params, run_context: nil) end |
#call_in_context(params, run_context:) ⇒ Object
Same as #call but forwards a run_context to the SkillContext so the skill’s execute block can reach ‘ctx.run_context` (the host application’s per-run state: current actor, ticket, request id, whatever the host subclassed Kernai::Context to carry).
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/kernai/skill.rb', line 184 def call_in_context(params, run_context:) validated = validate_params(params) # Arity compat: legacy skills take `|params|` only. New skills # opt into `|params, ctx|` to reach credentials/config AND the # host's per-run Context (via `ctx.run_context`). We pass the # context only when the block can accept it. # |p| → arity 1 → legacy # |p, c| → arity 2 → new # |p, *rest| → arity -2 → new (splat can absorb ctx) arity = @execute_block.arity if arity >= 2 || arity <= -2 @execute_block.call(validated, SkillContext.new(self, run_context: run_context)) else @execute_block.call(validated) end end |
#config(name, type = String, default: nil, description: nil) ⇒ Object
Declare a non-secret configuration value. Visible in the skill’s description (so the agent knows the knob exists) but its resolved value is never included in any LLM-facing rendering — only the execute block sees the value, via ‘ctx.config(:key)`.
131 132 133 |
# File 'lib/kernai/skill.rb', line 131 def config(name, type = String, default: nil, description: nil) @configs[name.to_sym] = { type: type, default: default, description: description } end |
#credential(name, required: false, description: nil) ⇒ Object
Declare a credential the skill needs. Credentials are never rendered with their resolved values; only the declaration (name + required flag) is visible. The execute block reads the value via ‘ctx.credential(:key)`, which validates `required: true` at access time.
140 141 142 |
# File 'lib/kernai/skill.rb', line 140 def credential(name, required: false, description: nil) @credentials[name.to_sym] = { required: required, description: description } end |
#description(text) ⇒ Object
91 92 93 |
# File 'lib/kernai/skill.rb', line 91 def description(text) @description_text = text end |
#execute(&block) ⇒ Object
144 145 146 |
# File 'lib/kernai/skill.rb', line 144 def execute(&block) @execute_block = block end |
#input(name, type, default: NO_DEFAULT, of: nil, schema: nil) ⇒ Object
Declare an input.
Type grammar:
Class → value.is_a?(Class)
[Class, Class, ...] → union; value.is_a?(any of the classes)
Array, of: ELEM → array where every element matches ELEM
Hash, schema: { ... } → object with the given per-key typing
‘ELEM` and `schema` entries share a grammar:
Class → required scalar of that class
[Class, Class] → required union scalar
Hash (shorthand schema) → required nested object (Hash {...})
{ type:, default:, of:, schema: } → full spec hash
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/kernai/skill.rb', line 108 def input(name, type, default: NO_DEFAULT, of: nil, schema: nil) if of && !type_includes?(type, Array) raise ArgumentError, "Invalid input spec for :#{name} — `of:` requires type Array (or a union containing Array)" end if schema && !type_includes?(type, Hash) raise ArgumentError, "Invalid input spec for :#{name} — `schema:` requires type Hash (or a union containing Hash)" end raise ArgumentError, "Invalid input spec for :#{name} — unsupported type #{type.inspect}" unless valid_type?(type) raise ArgumentError, "Invalid element spec for :#{name} — #{of.inspect}" if of && !valid_element_spec?(of) spec = { type: type, default: default } spec[:of] = of if of spec[:schema] = schema if schema @inputs[name] = spec end |
#produces(*kinds) ⇒ Object
Declare the media kinds the skill may emit back into the conversation (e.g. ‘produces :image` for an image-generation tool). Purely informational for now — the kernel uses it to inform the instruction builder, and future providers may route the result through an appropriate output channel.
160 161 162 |
# File 'lib/kernai/skill.rb', line 160 def produces(*kinds) @produced_kinds.concat(kinds.flatten.map(&:to_sym)) end |
#requires(*caps) ⇒ Object
Declare the model capabilities the skill needs to be runnable. A skill that consumes images in its prompt should ‘requires :vision` so it stays hidden from text-only models. Multiple calls accumulate.
151 152 153 |
# File 'lib/kernai/skill.rb', line 151 def requires(*caps) @required_capabilities.concat(caps.flatten.map(&:to_sym)) end |
#runnable_on?(model) ⇒ Boolean
A skill is runnable on a given model when that model satisfies every capability declared via ‘requires`. Skills with no requirements run everywhere.
167 168 169 170 171 |
# File 'lib/kernai/skill.rb', line 167 def runnable_on?(model) return true if @required_capabilities.empty? model.supports?(*@required_capabilities) end |
#to_description ⇒ Object
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/kernai/skill.rb', line 202 def to_description parts = ["- #{@name}"] parts << ": #{@description_text}" if @description_text if @inputs.any? parts << "\n Inputs: #{render_inputs_summary}" parts << "\n Usage: #{usage_example}" end if @configs.any? configs_str = @configs.map do |name, spec| str = "#{name} (#{spec[:type].name})" str += " default: #{spec[:default]}" unless spec[:default].nil? str end.join(', ') parts << "\n Config: #{configs_str}" end # IMPORTANT: credentials never include resolved values in this # rendering. Declaring them here just signals to the agent that # the skill is backed by configured secrets (so it understands # why a CredentialMissingError might happen at runtime). if @credentials.any? creds_str = @credentials.map do |name, spec| spec[:required] ? "#{name} (required)" : name.to_s end.join(', ') parts << "\n Credentials: #{creds_str}" end parts.join end |