Class: Insika::SkillStore

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/skill_store.rb

Overview

AUTHORED skills, in two scopes. Holds the complete SKILL.md (frontmatter + body) in the durable Store. The SkillCatalog overlays these skills on top of the on-disk ones (seed), with the Store winning — so editing/creating a skill in the Studio takes effect without a restart (via reload).

SHARED scope (agent: omitted) — one record per skill in the ConfigStore (scope "skills"), keyed by the skill name:

{ "content" => "<entire SKILL.md>",
"updated_at" => iso8601,
"history" => [ { "content" =>, "at" => }, ... ] }

AGENT scope (agent: given) — one record per AGENT (scope "agent_skills"), the skills nested under it, exactly the AgentFileStore shape:

{ "skills" => { "<name>" => { "content" =>, "updated_at" =>, "history" => [] } } }

The agent dimension is a SECOND ARGUMENT, never part of the key. A composite "agent/name" key would put a / inside what the Studio serves as a single path segment (GET /skills/:name, the editor, the versions list) — the class of route bug that ships green and 404s in production. Two arguments become two route segments (/agents/:id/skills/:name) and nothing needs encoding.

Two scopes and not one: the shared records are untouched by the arrival of the agent dimension, so there is no migration and a live deployment keeps serving exactly what it served.

THE STORE POSITION IS THE IDENTITY. Which scope a record sits in — and under which key — is what decides which skill it is; the frontmatter name: inside an override stays the bare shared name. See SkillCatalog#find.

Constant Summary collapse

SCOPE =
"skills"
AGENT_SCOPE =
"agent_skills"
HISTORY_MAX =
20

Instance Method Summary collapse

Constructor Details

#initialize(config_store:) ⇒ SkillStore

Returns a new instance of SkillStore.



39
40
41
# File 'lib/insika/skill_store.rb', line 39

def initialize(config_store:)
  @cs = config_store
end

Instance Method Details

#agentsObject

-> [String] every agent that has specialized at least one skill. What the catalog overlays and doctor sweeps.



60
# File 'lib/insika/skill_store.rb', line 60

def agents = @cs.keys(AGENT_SCOPE).sort

#all(agent: nil) ⇒ Object

-> { name => content } of the scope's authored skills.



54
55
56
# File 'lib/insika/skill_store.rb', line 54

def all(agent: nil)
  names(agent: agent).each_with_object({}) { |n, acc| acc[n] = get(n, agent: agent) }
end

#delete(name, agent: nil) ⇒ Object

-> bool (did it exist?).



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/insika/skill_store.rb', line 74

def delete(name, agent: nil)
  key = name.to_s
  return @cs.delete(SCOPE, key) if agent.nil?

  wrapper = @cs.get(AGENT_SCOPE, agent.to_s)
  return false unless wrapper&.dig("skills", key)

  wrapper["skills"].delete(key)
  @cs.put(AGENT_SCOPE, agent.to_s, wrapper)
  true
end

#get(name, agent: nil) ⇒ Object

-> String | nil (complete SKILL.md).



44
45
46
# File 'lib/insika/skill_store.rb', line 44

def get(name, agent: nil)
  record(name, agent)&.fetch("content", nil)
end

#names(agent: nil) ⇒ Object

-> [String] names in the scope, lexicographic order.



49
50
51
# File 'lib/insika/skill_store.rb', line 49

def names(agent: nil)
  agent.nil? ? @cs.keys(SCOPE) : agent_skills(agent).keys.sort
end

#restore(name, index, agent: nil) ⇒ Object

Restores version index as the current content (a new write). -> Hash.



90
91
92
93
94
95
96
97
# File 'lib/insika/skill_store.rb', line 90

def restore(name, index, agent: nil)
  hist = versions(name, agent: agent)
  i = Integer(index)
  raise Insika::NotFoundError, "skill '#{name}' not found" unless record(name.to_s, agent)
  raise Insika::ValidationError, "version #{index} does not exist" if i.negative? || i >= hist.length

  write(name, hist[i]["content"], agent: agent)
end

#versions(name, agent: nil) ⇒ Object

-> [ { "content" =>, "at" => } ] most recent first.



87
# File 'lib/insika/skill_store.rb', line 87

def versions(name, agent: nil) = record(name, agent)&.fetch("history", []) || []

#write(name, content, agent: nil, create_only: false) ⇒ Object

Writes (upsert). create_only refuses to overwrite. -> Hash (the stored record).



63
64
65
66
67
68
69
70
71
# File 'lib/insika/skill_store.rb', line 63

def write(name, content, agent: nil, create_only: false)
  key = name.to_s
  current = record(key, agent)
  raise Insika::ValidationError, "skill '#{key}' already exists#{" for agent '#{agent}'" if agent}" if create_only && current

  rec = build_record(content.to_s, current)
  put(key, rec, agent)
  rec
end