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:, avatar: }



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
105
106
107
# 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, 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: author,
      avatar: avatar,
    }
  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?
    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"))
    meta = read_profile_yml(File.join(path, "profile.yml"))
    user_avatar = File.file?(File.join(path, "avatar.png")) ? "/agent_avatar/#{id}" : nil
    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, user_avatar
    )
  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)



122
123
124
125
# File 'lib/clacky/agent_profile.rb', line 122

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)



128
129
130
131
132
133
134
135
# File 'lib/clacky/agent_profile.rb', line 128

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



117
118
119
# File 'lib/clacky/agent_profile.rb', line 117

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)



138
139
140
141
142
143
144
145
# File 'lib/clacky/agent_profile.rb', line 138

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