Class: Kernai::Skill

Inherits:
Object
  • Object
show all
Defined in:
lib/kernai/skill.rb

Constant Summary collapse

NO_DEFAULT =
:__no_default__

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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

#configsObject (readonly)

Returns the value of attribute configs.



5
6
7
# File 'lib/kernai/skill.rb', line 5

def configs
  @configs
end

#credentialsObject (readonly)

Returns the value of attribute credentials.



5
6
7
# File 'lib/kernai/skill.rb', line 5

def credentials
  @credentials
end

#description_textObject (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_blockObject (readonly)

Returns the value of attribute execute_block.



5
6
7
# File 'lib/kernai/skill.rb', line 5

def execute_block
  @execute_block
end

#inputsObject (readonly)

Returns the value of attribute inputs.



5
6
7
# File 'lib/kernai/skill.rb', line 5

def inputs
  @inputs
end

#nameObject (readonly)

Returns the value of attribute name.



5
6
7
# File 'lib/kernai/skill.rb', line 5

def name
  @name
end

#produced_kindsObject (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_capabilitiesObject (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

.allObject



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



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/kernai/skill.rb', line 170

def call(params = {})
  validated = validate_params(params)

  # Arity compat: legacy skills take `|params|` only. New skills
  # opt into `|params, ctx|` to reach credentials/config. 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))
  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)`.



128
129
130
# File 'lib/kernai/skill.rb', line 128

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.



137
138
139
# File 'lib/kernai/skill.rb', line 137

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



141
142
143
# File 'lib/kernai/skill.rb', line 141

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

Raises:

  • (ArgumentError)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/kernai/skill.rb', line 108

def input(name, type, default: NO_DEFAULT, of: nil, schema: nil)
  raise ArgumentError, "Invalid input spec for :#{name} — `of:` requires type Array" if of && type != Array

  if schema && type != Hash
    raise ArgumentError,
          "Invalid input spec for :#{name} — `schema:` requires type 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.



157
158
159
# File 'lib/kernai/skill.rb', line 157

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.



148
149
150
# File 'lib/kernai/skill.rb', line 148

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.

Returns:

  • (Boolean)


164
165
166
167
168
# File 'lib/kernai/skill.rb', line 164

def runnable_on?(model)
  return true if @required_capabilities.empty?

  model.supports?(*@required_capabilities)
end

#to_descriptionObject



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/kernai/skill.rb', line 187

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