Class: Clacky::AgentProfile
- Inherits:
-
Object
- Object
- Clacky::AgentProfile
- Defined in:
- lib/clacky/agent_profile.rb
Overview
Loads and represents an agent profile (system prompt + skill whitelist).
Lookup order for a profile named "coding":
1. ~/.clacky/agents/coding/ (user override, physical dir)
2. extension agent unit with id == "coding" (ext.yml contributes.agents)
Each user profile directory (opt-in override) contains:
- profile.yml — name, description, skills whitelist
- system_prompt.md — agent-specific system prompt content
Global files (shared across all agents) are user-only overrides:
- ~/.clacky/agents/SOUL.md — agent personality/values (else DEFAULT_SOUL)
- ~/.clacky/agents/USER.md — user profile info (else DEFAULT_USER)
The universal behavioural rules (todo manager, tool usage, response style, etc.) live in a bundled resource file at lib/clacky/prompts/base.md.
Constant Summary collapse
- USER_AGENTS_DIR =
File.("~/.clacky/agents").freeze
- BASE_PROMPT_PATH =
File.("../prompts/base.md", __FILE__).freeze
- DEFAULT_SOUL =
<<~MD.freeze You are calm, precise, and helpful. You communicate clearly and concisely. You are honest about uncertainty and ask for clarification when needed. You take initiative but respect the user's preferences and decisions. MD
- DEFAULT_USER =
"(No user profile configured yet. To personalize, create ~/.clacky/agents/USER.md)"
Instance Attribute Summary collapse
-
#description ⇒ Object
readonly
Returns the value of attribute description.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Class Method Summary collapse
-
.all(recency: {}) ⇒ Array<Hash>
List all available agent profiles across user + extension layers.
- .load(name) ⇒ AgentProfile
Instance Method Summary collapse
-
#base_prompt ⇒ String
Base prompt shared by all agents (bundled resource).
-
#container_dir ⇒ String?
Directory of the ext container this agent belongs to, or nil for a user profile.
-
#disabled_skills ⇒ Array<String>
Skills explicitly disabled for this agent, matched by skill identifier.
-
#initialize(name) ⇒ AgentProfile
constructor
A new instance of AgentProfile.
-
#skill_allowed?(skill) ⇒ Boolean
Whether a skill is usable by this agent: it must not be in the agent's disabled list AND must be allowed by the skill's own
agent:declaration. -
#soul ⇒ String
Soul content (user override, else default).
-
#system_prompt ⇒ String
Agent-specific system prompt content.
-
#tools ⇒ Array<String>
Extension tools this agent opts into, via
tools:in profile.yml or the ext agent spec — the only way a contributed tool reaches an agent. -
#user_profile ⇒ String
User profile content (user override, else default).
Constructor Details
#initialize(name) ⇒ AgentProfile
Returns a new instance of AgentProfile.
35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/clacky/agent_profile.rb', line 35 def initialize(name) @name = name.to_s result = ExtensionLoader.last_result @ext_unit = result&.agents&.find { |u| u.id == @name } if @ext_unit.nil? result = ExtensionLoader.load_all(force: true) @ext_unit = result&.agents&.find { |u| u.id == @name } end @profile_data = load_profile_yml @description = @profile_data["description"] || "" @system_prompt_content = load_agent_file("system_prompt.md") end |
Instance Attribute Details
#description ⇒ Object (readonly)
Returns the value of attribute description.
33 34 35 |
# File 'lib/clacky/agent_profile.rb', line 33 def description @description end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
33 34 35 |
# File 'lib/clacky/agent_profile.rb', line 33 def name @name end |
Class Method Details
.all(recency: {}) ⇒ Array<Hash>
List all available agent profiles across user + extension layers. Precedence on id collision: user override → extension unit.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/clacky/agent_profile.rb', line 92 def self.all(recency: {}) out = {} add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, , avatar| next if id.nil? || id.empty? out[id] = { id: id, title: title, title_zh: title_zh, description: description, description_zh: description_zh, source: source, order: order, layer: layer, author: , avatar: avatar, } end ext_result = ExtensionLoader.last_result || ExtensionLoader.load_all ext_result&.agents&.each do |unit| spec = unit.spec || {} next if spec["hidden"] title = spec["title"].to_s title = unit.id if title.empty? avatar = spec["avatar_abs"].to_s.empty? ? nil : "/agent_avatar/#{unit.id}" add.call( unit.id, title, spec["title_zh"].to_s, spec["description"].to_s, spec["description_zh"].to_s, "extension", spec["order"], unit.layer.to_s, spec["author"].to_s, avatar ) end Dir.glob(File.join(USER_AGENTS_DIR, "*")).sort.each do |path| next unless File.directory?(path) id = File.basename(path) next if id.start_with?("_") next unless File.file?(File.join(path, "profile.yml")) = read_profile_yml(File.join(path, "profile.yml")) user_avatar = File.file?(File.join(path, "avatar.png")) ? "/agent_avatar/#{id}" : nil add.call( id, ["title"] || ["name"] || id, ["title_zh"].to_s, ["description"].to_s, ["description_zh"].to_s, "user", ["order"], "user", ["author"].to_s.empty? ? "You" : ["author"].to_s, user_avatar ) end # Builtin agents always come first, then user overrides, then third-party # extension agents — so the "start new session" grid keeps the official # agents grouped ahead of anything the user installed from the market. # Within the third-party group, agents are ordered by most-recently-used # first (via `recency`), falling back to their static `order` field for # agents that have never been used yet. layer_rank = lambda do |a| next 0 if a[:layer] == "builtin" next 1 if a[:source] == "user" 2 end out.values.sort_by do |a| rank = layer_rank.call(a) if rank == 2 [rank, -(recency[a[:id]] || 0), a[:order] || 999, a[:id]] else [rank, a[:order] || 999, a[:id]] end end end |
.load(name) ⇒ AgentProfile
82 83 84 |
# File 'lib/clacky/agent_profile.rb', line 82 def self.load(name) new(name) end |
Instance Method Details
#base_prompt ⇒ String
Returns base prompt shared by all agents (bundled resource).
176 177 178 179 |
# File 'lib/clacky/agent_profile.rb', line 176 def base_prompt return "" unless File.file?(BASE_PROMPT_PATH) File.read(BASE_PROMPT_PATH).strip end |
#container_dir ⇒ String?
Directory of the ext container this agent belongs to, or nil for a user profile. Extension tools only exist inside containers, so a user profile cannot reference them.
67 68 69 |
# File 'lib/clacky/agent_profile.rb', line 67 def container_dir @ext_unit&.dir end |
#disabled_skills ⇒ Array<String>
Skills explicitly disabled for this agent, matched by skill identifier.
Declared via disabled_skills in profile.yml or the ext agent spec.
51 52 53 |
# File 'lib/clacky/agent_profile.rb', line 51 def disabled_skills @disabled_skills ||= Array(@profile_data["disabled_skills"]).map(&:to_s) end |
#skill_allowed?(skill) ⇒ Boolean
Whether a skill is usable by this agent: it must not be in the agent's
disabled list AND must be allowed by the skill's own agent: declaration.
75 76 77 78 |
# File 'lib/clacky/agent_profile.rb', line 75 def skill_allowed?(skill) return false if disabled_skills.include?(skill.identifier) skill.allowed_for_agent?(name) end |
#soul ⇒ String
Returns soul content (user override, else default).
182 183 184 185 186 187 188 189 |
# File 'lib/clacky/agent_profile.rb', line 182 def soul user_path = File.join(USER_AGENTS_DIR, "SOUL.md") if File.exist?(user_path) && !File.zero?(user_path) File.read(user_path).strip else DEFAULT_SOUL.strip end end |
#system_prompt ⇒ String
Returns agent-specific system prompt content.
171 172 173 |
# File 'lib/clacky/agent_profile.rb', line 171 def system_prompt @system_prompt_content end |
#tools ⇒ Array<String>
Extension tools this agent opts into, via tools: in profile.yml or the
ext agent spec — the only way a contributed tool reaches an agent. Each
id is a bare tool id in the agent's own container: tools/<id>.rb.
59 60 61 |
# File 'lib/clacky/agent_profile.rb', line 59 def tools @tools ||= Array(@profile_data["tools"]).map(&:to_s) end |
#user_profile ⇒ String
Returns user profile content (user override, else default).
192 193 194 195 196 197 198 199 |
# File 'lib/clacky/agent_profile.rb', line 192 def user_profile user_path = File.join(USER_AGENTS_DIR, "USER.md") if File.exist?(user_path) && !File.zero?(user_path) File.read(user_path).strip else DEFAULT_USER end end |