Class: Clacky::AgentProfile

Inherits:
Object
  • Object
show all
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.expand_path("~/.clacky/agents").freeze
BASE_PROMPT_PATH =
File.expand_path("../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

Class Method Summary collapse

Instance Method Summary collapse

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

#descriptionObject (readonly)

Returns the value of attribute description.



33
34
35
# File 'lib/clacky/agent_profile.rb', line 33

def description
  @description
end

#nameObject (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

.allArray<Hash>

List all available agent profiles across user + extension layers. Precedence on id collision: user override → extension unit.

Returns:

  • (Array<Hash>)

    each: { id:, title:, title_zh:, description:, description_zh:, source:, order:, layer:, author: }



57
58
59
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
# File 'lib/clacky/agent_profile.rb', line 57

def self.all
  out = {}

  add = lambda do |id, title, title_zh, description, description_zh, source, order, layer, author|
    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: author,
    }
  end

  ext_result = ExtensionLoader.last_result || ExtensionLoader.load_all
  ext_result&.agents&.each do |unit|
    spec = unit.spec || {}
    title = spec["title"].to_s
    title = unit.id if title.empty?
    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
    )
  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"))
    meta = read_profile_yml(File.join(path, "profile.yml"))
    add.call(
      id, meta["title"] || meta["name"] || id, meta["title_zh"].to_s,
      meta["description"].to_s, meta["description_zh"].to_s,
      "user", meta["order"], "user",
      meta["author"].to_s.empty? ? "You" : meta["author"].to_s
    )
  end

  source_rank = { "user" => 0, "extension" => 1 }
  out.values.sort_by { |a| [source_rank[a[:source]] || 9, a[:order] || 999, a[:id]] }
end

.load(name) ⇒ AgentProfile

Parameters:

  • name (String, Symbol)

    profile name (e.g. "coding", "general")

Returns:



50
51
52
# File 'lib/clacky/agent_profile.rb', line 50

def self.load(name)
  new(name)
end

Instance Method Details

#base_promptString

Returns base prompt shared by all agents (bundled resource).

Returns:

  • (String)

    base prompt shared by all agents (bundled resource)



119
120
121
122
# File 'lib/clacky/agent_profile.rb', line 119

def base_prompt
  return "" unless File.file?(BASE_PROMPT_PATH)
  File.read(BASE_PROMPT_PATH).strip
end

#soulString

Returns soul content (user override, else default).

Returns:

  • (String)

    soul content (user override, else default)



125
126
127
128
129
130
131
132
# File 'lib/clacky/agent_profile.rb', line 125

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_promptString

Returns agent-specific system prompt content.

Returns:

  • (String)

    agent-specific system prompt content



114
115
116
# File 'lib/clacky/agent_profile.rb', line 114

def system_prompt
  @system_prompt_content
end

#user_profileString

Returns user profile content (user override, else default).

Returns:

  • (String)

    user profile content (user override, else default)



135
136
137
138
139
140
141
142
# File 'lib/clacky/agent_profile.rb', line 135

def 
  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