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).
-
#initialize(name) ⇒ AgentProfile
constructor
A new instance of AgentProfile.
-
#soul ⇒ String
Soul content (user override, else default).
-
#system_prompt ⇒ String
Agent-specific system prompt content.
-
#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.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 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 |
# File 'lib/clacky/agent_profile.rb', line 60 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
50 51 52 |
# File 'lib/clacky/agent_profile.rb', line 50 def self.load(name) new(name) end |
Instance Method Details
#base_prompt ⇒ String
Returns base prompt shared by all agents (bundled resource).
144 145 146 147 |
# File 'lib/clacky/agent_profile.rb', line 144 def base_prompt return "" unless File.file?(BASE_PROMPT_PATH) File.read(BASE_PROMPT_PATH).strip end |
#soul ⇒ String
Returns soul content (user override, else default).
150 151 152 153 154 155 156 157 |
# File 'lib/clacky/agent_profile.rb', line 150 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.
139 140 141 |
# File 'lib/clacky/agent_profile.rb', line 139 def system_prompt @system_prompt_content end |
#user_profile ⇒ String
Returns user profile content (user override, else default).
160 161 162 163 164 165 166 167 |
# File 'lib/clacky/agent_profile.rb', line 160 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 |