anima-core 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +8 -0
  3. data/agents/codebase-analyzer.md +1 -1
  4. data/agents/codebase-pattern-finder.md +1 -1
  5. data/agents/documentation-researcher.md +1 -1
  6. data/agents/thoughts-analyzer.md +1 -1
  7. data/agents/web-search-researcher.md +1 -1
  8. data/app/channels/session_channel.rb +46 -49
  9. data/app/decorators/agent_message_decorator.rb +2 -2
  10. data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
  11. data/app/decorators/system_message_decorator.rb +2 -2
  12. data/app/decorators/tool_call_decorator.rb +2 -2
  13. data/app/decorators/tool_decorator.rb +4 -4
  14. data/app/decorators/tool_response_decorator.rb +2 -2
  15. data/app/decorators/user_message_decorator.rb +3 -3
  16. data/app/decorators/web_get_tool_decorator.rb +41 -9
  17. data/app/jobs/agent_request_job.rb +20 -20
  18. data/app/jobs/count_message_tokens_job.rb +39 -0
  19. data/app/jobs/passive_recall_job.rb +4 -4
  20. data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
  21. data/app/models/goal.rb +4 -4
  22. data/app/models/goal_pinned_message.rb +11 -0
  23. data/app/models/message.rb +132 -0
  24. data/app/models/pinned_message.rb +41 -0
  25. data/app/models/session.rb +232 -192
  26. data/app/models/snapshot.rb +25 -25
  27. data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
  28. data/lib/agent_loop.rb +17 -9
  29. data/lib/agents/registry.rb +1 -1
  30. data/lib/analytical_brain/runner.rb +35 -35
  31. data/lib/analytical_brain/tools/activate_skill.rb +5 -9
  32. data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
  34. data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
  35. data/lib/analytical_brain/tools/finish_goal.rb +5 -8
  36. data/lib/analytical_brain/tools/read_workflow.rb +5 -9
  37. data/lib/analytical_brain/tools/rename_session.rb +3 -10
  38. data/lib/analytical_brain/tools/set_goal.rb +3 -7
  39. data/lib/analytical_brain/tools/update_goal.rb +3 -7
  40. data/lib/anima/settings.rb +19 -4
  41. data/lib/anima/version.rb +1 -1
  42. data/lib/events/bounce_back.rb +7 -7
  43. data/lib/events/subscribers/persister.rb +7 -7
  44. data/lib/events/subscribers/subagent_message_router.rb +12 -12
  45. data/lib/events/subscribers/transient_broadcaster.rb +2 -2
  46. data/lib/llm/client.rb +5 -2
  47. data/lib/mneme/compressed_viewport.rb +57 -57
  48. data/lib/mneme/l2_runner.rb +4 -4
  49. data/lib/mneme/passive_recall.rb +2 -2
  50. data/lib/mneme/runner.rb +57 -75
  51. data/lib/mneme/search.rb +55 -38
  52. data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
  53. data/lib/mneme/tools/everything_ok.rb +1 -3
  54. data/lib/mneme/tools/save_snapshot.rb +12 -16
  55. data/lib/skills/registry.rb +1 -1
  56. data/lib/tools/bash.rb +82 -7
  57. data/lib/tools/edit.rb +4 -6
  58. data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
  59. data/lib/tools/read.rb +4 -4
  60. data/lib/tools/registry.rb +1 -1
  61. data/lib/tools/remember.rb +46 -55
  62. data/lib/tools/spawn_specialist.rb +12 -23
  63. data/lib/tools/spawn_subagent.rb +9 -19
  64. data/lib/tools/subagent_prompts.rb +0 -2
  65. data/lib/tools/think.rb +3 -10
  66. data/lib/tools/web_get.rb +23 -4
  67. data/lib/tools/write.rb +3 -3
  68. data/lib/tui/cable_client.rb +3 -3
  69. data/lib/tui/message_store.rb +37 -37
  70. data/lib/tui/screens/chat.rb +27 -15
  71. data/lib/workflows/registry.rb +1 -1
  72. data/skills/activerecord/SKILL.md +1 -1
  73. data/skills/dragonruby/SKILL.md +1 -1
  74. data/skills/draper-decorators/SKILL.md +1 -1
  75. data/skills/gh-issue.md +1 -1
  76. data/skills/mcp-server/SKILL.md +1 -1
  77. data/skills/ratatui-ruby/SKILL.md +1 -1
  78. data/skills/rspec/SKILL.md +1 -1
  79. data/templates/config.toml +21 -4
  80. data/templates/soul.md +7 -19
  81. data/workflows/create_handoff.md +1 -1
  82. data/workflows/create_note.md +1 -1
  83. data/workflows/create_plan.md +1 -1
  84. data/workflows/implement_plan.md +1 -1
  85. data/workflows/iterate_plan.md +1 -1
  86. data/workflows/research_codebase.md +1 -1
  87. data/workflows/resume_handoff.md +1 -1
  88. data/workflows/review_pr.md +78 -16
  89. data/workflows/thoughts_init.md +1 -1
  90. data/workflows/validate_plan.md +1 -1
  91. metadata +10 -9
  92. data/app/jobs/count_event_tokens_job.rb +0 -39
  93. data/app/models/event.rb +0 -110
  94. data/app/models/goal_pinned_event.rb +0 -11
  95. data/app/models/pinned_event.rb +0 -41
  96. data/lib/mneme/tools/attach_events_to_goals.rb +0 -107
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # A persisted summary of conversation context created by Mneme before
4
- # events evict from the viewport. Snapshots capture the "gist" of what
4
+ # messages evict from the viewport. Snapshots capture the "gist" of what
5
5
  # happened so the agent retains awareness of past context.
6
6
  #
7
- # Level 1 snapshots are created from raw events (messages + thinks).
7
+ # Level 1 snapshots are created from raw messages (conversation + thinks).
8
8
  # Level 2 snapshots compress multiple Level 1 snapshots (days/weeks scale).
9
- # Both levels use the same event ID range tracking — an L2 snapshot's range
9
+ # Both levels use the same message ID range tracking — an L2 snapshot's range
10
10
  # is the union of its constituent L1 snapshots.
11
11
  #
12
12
  # @!attribute text
13
13
  # @return [String] the summary text generated by Mneme
14
- # @!attribute from_event_id
15
- # @return [Integer] first event ID covered by this snapshot
16
- # @!attribute to_event_id
17
- # @return [Integer] last event ID covered by this snapshot
14
+ # @!attribute from_message_id
15
+ # @return [Integer] first message ID covered by this snapshot
16
+ # @!attribute to_message_id
17
+ # @return [Integer] last message ID covered by this snapshot
18
18
  # @!attribute level
19
- # @return [Integer] compression level (1 = from raw events, 2 = from L1 snapshots)
19
+ # @return [Integer] compression level (1 = from raw messages, 2 = from L1 snapshots)
20
20
  # @!attribute token_count
21
21
  # @return [Integer] cached token count of the summary text
22
22
  class Snapshot < ApplicationRecord
@@ -27,34 +27,34 @@ class Snapshot < ApplicationRecord
27
27
  MAX_TEXT_BYTES = 32_768
28
28
 
29
29
  validates :text, presence: true, length: {maximum: MAX_TEXT_BYTES}
30
- validates :from_event_id, presence: true
31
- validates :to_event_id, presence: true
30
+ validates :from_message_id, presence: true
31
+ validates :to_message_id, presence: true
32
32
  validates :level, presence: true, numericality: {greater_than: 0}
33
33
  validates :token_count, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
34
- validate :from_event_id_not_after_to_event_id
34
+ validate :from_message_id_not_after_to_message_id
35
35
 
36
36
  scope :for_level, ->(level) { where(level: level) }
37
- scope :chronological, -> { order(:from_event_id) }
37
+ scope :chronological, -> { order(:from_message_id) }
38
38
 
39
- # L1 snapshots whose event range is NOT fully contained within any L2 snapshot.
39
+ # L1 snapshots whose message range is NOT fully contained within any L2 snapshot.
40
40
  # Used to determine which L1 snapshots are still "live" in the viewport.
41
41
  scope :not_covered_by_l2, -> {
42
42
  where.not(
43
43
  "EXISTS (SELECT 1 FROM snapshots l2 " \
44
44
  "WHERE l2.session_id = snapshots.session_id " \
45
45
  "AND l2.level = 2 " \
46
- "AND l2.from_event_id <= snapshots.from_event_id " \
47
- "AND l2.to_event_id >= snapshots.to_event_id)"
46
+ "AND l2.from_message_id <= snapshots.from_message_id " \
47
+ "AND l2.to_message_id >= snapshots.to_message_id)"
48
48
  )
49
49
  }
50
50
 
51
- # Snapshots whose source events have fully evicted from the sliding window.
52
- # A snapshot is visible when its entire event range precedes the first
53
- # event currently in the viewport.
51
+ # Snapshots whose source messages have fully evicted from the sliding window.
52
+ # A snapshot is visible when its entire message range precedes the first
53
+ # message currently in the viewport.
54
54
  #
55
- # @param first_event_id [Integer] the first event ID in the sliding window
56
- scope :source_events_evicted, ->(first_event_id) {
57
- where("to_event_id < ?", first_event_id)
55
+ # @param first_message_id [Integer] the first message ID in the sliding window
56
+ scope :source_messages_evicted, ->(first_message_id) {
57
+ where("to_message_id < ?", first_message_id)
58
58
  }
59
59
 
60
60
  # @return [Integer] token cost, using cached count or heuristic estimate
@@ -64,13 +64,13 @@ class Snapshot < ApplicationRecord
64
64
 
65
65
  private
66
66
 
67
- def from_event_id_not_after_to_event_id
68
- return unless from_event_id && to_event_id
69
- errors.add(:from_event_id, "must be <= to_event_id") if from_event_id > to_event_id
67
+ def from_message_id_not_after_to_message_id
68
+ return unless from_message_id && to_message_id
69
+ errors.add(:from_message_id, "must be <= to_message_id") if from_message_id > to_message_id
70
70
  end
71
71
 
72
72
  # @return [Integer] estimated token count (at least 1)
73
73
  def estimate_tokens
74
- [(text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
74
+ [(text.bytesize / Message::BYTES_PER_TOKEN.to_f).ceil, 1].max
75
75
  end
76
76
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Renames the persistence layer from Event to Message.
4
+ #
5
+ # Events are ephemeral bus signals; Messages are persisted records of what
6
+ # was said, by whom, in which conversation. The Anthropic API calls them
7
+ # "messages" — this rename aligns the codebase with the agent's vocabulary.
8
+ #
9
+ # See: thoughts/shared/notes/2026-03-24/narrative-design-for-agent-prompts.md
10
+ class RenameEventToMessage < ActiveRecord::Migration[8.1]
11
+ def up
12
+ # --- FTS5: drop triggers and virtual table (references old table name) ---
13
+ execute "DROP TRIGGER IF EXISTS events_fts_insert"
14
+ execute "DROP TRIGGER IF EXISTS events_fts_delete"
15
+ execute "DROP TABLE IF EXISTS events_fts"
16
+
17
+ # --- Main table: events → messages ---
18
+ rename_table :events, :messages
19
+ rename_column :messages, :event_type, :message_type
20
+
21
+ # --- Pinned events → pinned messages ---
22
+ rename_table :pinned_events, :pinned_messages
23
+ rename_column :pinned_messages, :event_id, :message_id
24
+
25
+ # --- Join table: goal_pinned_events → goal_pinned_messages ---
26
+ rename_table :goal_pinned_events, :goal_pinned_messages
27
+ rename_column :goal_pinned_messages, :pinned_event_id, :pinned_message_id
28
+
29
+ # --- Snapshots: event range columns ---
30
+ rename_column :snapshots, :from_event_id, :from_message_id
31
+ rename_column :snapshots, :to_event_id, :to_message_id
32
+
33
+ # --- Sessions: event pointer columns ---
34
+ rename_column :sessions, :mneme_boundary_event_id, :mneme_boundary_message_id
35
+ rename_column :sessions, :mneme_snapshot_first_event_id, :mneme_snapshot_first_message_id
36
+ rename_column :sessions, :mneme_snapshot_last_event_id, :mneme_snapshot_last_message_id
37
+ rename_column :sessions, :recalled_event_ids, :recalled_message_ids
38
+ rename_column :sessions, :viewport_event_ids, :viewport_message_ids
39
+
40
+ # --- Recreate FTS5 virtual table with new names ---
41
+ execute <<~SQL
42
+ CREATE VIRTUAL TABLE messages_fts USING fts5(
43
+ searchable_text,
44
+ content='',
45
+ contentless_delete=1,
46
+ tokenize='porter unicode61'
47
+ );
48
+ SQL
49
+
50
+ execute <<~SQL
51
+ INSERT INTO messages_fts(rowid, searchable_text)
52
+ SELECT m.id,
53
+ CASE
54
+ WHEN m.message_type IN ('user_message', 'agent_message', 'system_message')
55
+ THEN json_extract(m.payload, '$.content')
56
+ WHEN m.message_type = 'tool_call' AND json_extract(m.payload, '$.tool_name') = 'think'
57
+ THEN json_extract(m.payload, '$.tool_input.thoughts')
58
+ END
59
+ FROM messages m
60
+ WHERE (m.message_type IN ('user_message', 'agent_message', 'system_message'))
61
+ OR (m.message_type = 'tool_call' AND json_extract(m.payload, '$.tool_name') = 'think');
62
+ SQL
63
+
64
+ execute <<~SQL
65
+ CREATE TRIGGER messages_fts_insert AFTER INSERT ON messages
66
+ WHEN NEW.message_type IN ('user_message', 'agent_message', 'system_message')
67
+ OR (NEW.message_type = 'tool_call' AND json_extract(NEW.payload, '$.tool_name') = 'think')
68
+ BEGIN
69
+ INSERT INTO messages_fts(rowid, searchable_text)
70
+ VALUES (
71
+ NEW.id,
72
+ CASE
73
+ WHEN NEW.message_type IN ('user_message', 'agent_message', 'system_message')
74
+ THEN json_extract(NEW.payload, '$.content')
75
+ WHEN NEW.message_type = 'tool_call'
76
+ THEN json_extract(NEW.payload, '$.tool_input.thoughts')
77
+ END
78
+ );
79
+ END;
80
+ SQL
81
+
82
+ execute <<~SQL
83
+ CREATE TRIGGER messages_fts_delete AFTER DELETE ON messages
84
+ WHEN OLD.message_type IN ('user_message', 'agent_message', 'system_message')
85
+ OR (OLD.message_type = 'tool_call' AND json_extract(OLD.payload, '$.tool_name') = 'think')
86
+ BEGIN
87
+ DELETE FROM messages_fts WHERE rowid = OLD.id;
88
+ END;
89
+ SQL
90
+ end
91
+
92
+ def down
93
+ # --- FTS5: drop new triggers and table ---
94
+ execute "DROP TRIGGER IF EXISTS messages_fts_insert"
95
+ execute "DROP TRIGGER IF EXISTS messages_fts_delete"
96
+ execute "DROP TABLE IF EXISTS messages_fts"
97
+
98
+ # --- Sessions: restore event pointer columns ---
99
+ rename_column :sessions, :mneme_boundary_message_id, :mneme_boundary_event_id
100
+ rename_column :sessions, :mneme_snapshot_first_message_id, :mneme_snapshot_first_event_id
101
+ rename_column :sessions, :mneme_snapshot_last_message_id, :mneme_snapshot_last_event_id
102
+ rename_column :sessions, :recalled_message_ids, :recalled_event_ids
103
+ rename_column :sessions, :viewport_message_ids, :viewport_event_ids
104
+
105
+ # --- Snapshots: restore event range columns ---
106
+ rename_column :snapshots, :from_message_id, :from_event_id
107
+ rename_column :snapshots, :to_message_id, :to_event_id
108
+
109
+ # --- Join table: goal_pinned_messages → goal_pinned_events ---
110
+ rename_column :goal_pinned_messages, :pinned_message_id, :pinned_event_id
111
+ rename_table :goal_pinned_messages, :goal_pinned_events
112
+
113
+ # --- Pinned messages → pinned events ---
114
+ rename_column :pinned_messages, :message_id, :event_id
115
+ rename_table :pinned_messages, :pinned_events
116
+
117
+ # --- Main table: messages → events ---
118
+ rename_column :messages, :message_type, :event_type
119
+ rename_table :messages, :events
120
+
121
+ # --- Recreate original FTS5 ---
122
+ execute <<~SQL
123
+ CREATE VIRTUAL TABLE events_fts USING fts5(
124
+ searchable_text,
125
+ content='',
126
+ contentless_delete=1,
127
+ tokenize='porter unicode61'
128
+ );
129
+ SQL
130
+
131
+ execute <<~SQL
132
+ INSERT INTO events_fts(rowid, searchable_text)
133
+ SELECT e.id,
134
+ CASE
135
+ WHEN e.event_type IN ('user_message', 'agent_message', 'system_message')
136
+ THEN json_extract(e.payload, '$.content')
137
+ WHEN e.event_type = 'tool_call' AND json_extract(e.payload, '$.tool_name') = 'think'
138
+ THEN json_extract(e.payload, '$.tool_input.thoughts')
139
+ END
140
+ FROM events e
141
+ WHERE (e.event_type IN ('user_message', 'agent_message', 'system_message'))
142
+ OR (e.event_type = 'tool_call' AND json_extract(e.payload, '$.tool_name') = 'think');
143
+ SQL
144
+
145
+ execute <<~SQL
146
+ CREATE TRIGGER events_fts_insert AFTER INSERT ON events
147
+ WHEN NEW.event_type IN ('user_message', 'agent_message', 'system_message')
148
+ OR (NEW.event_type = 'tool_call' AND json_extract(NEW.payload, '$.tool_name') = 'think')
149
+ BEGIN
150
+ INSERT INTO events_fts(rowid, searchable_text)
151
+ VALUES (
152
+ NEW.id,
153
+ CASE
154
+ WHEN NEW.event_type IN ('user_message', 'agent_message', 'system_message')
155
+ THEN json_extract(NEW.payload, '$.content')
156
+ WHEN NEW.event_type = 'tool_call'
157
+ THEN json_extract(NEW.payload, '$.tool_input.thoughts')
158
+ END
159
+ );
160
+ END;
161
+ SQL
162
+
163
+ execute <<~SQL
164
+ CREATE TRIGGER events_fts_delete AFTER DELETE ON events
165
+ WHEN OLD.event_type IN ('user_message', 'agent_message', 'system_message')
166
+ OR (OLD.event_type = 'tool_call' AND json_extract(OLD.payload, '$.tool_name') = 'think')
167
+ BEGIN
168
+ DELETE FROM events_fts WHERE rowid = OLD.id;
169
+ END;
170
+ SQL
171
+ end
172
+ end
data/lib/agent_loop.rb CHANGED
@@ -42,7 +42,7 @@ class AgentLoop
42
42
 
43
43
  # Runs the agent loop for a single user input.
44
44
  #
45
- # Persists the user event directly (the global Persister skips
45
+ # Persists the user message directly (the global Persister skips
46
46
  # non-pending user messages because {AgentRequestJob} owns their
47
47
  # lifecycle). Then emits a bus notification and delegates to {#run}.
48
48
  # On error emits {Events::AgentMessage} with the error text.
@@ -53,7 +53,7 @@ class AgentLoop
53
53
  text = input.to_s.strip
54
54
  return if text.empty?
55
55
 
56
- persist_user_event(text)
56
+ persist_user_message(text)
57
57
  Events::Bus.emit(Events::UserMessage.new(content: text, session_id: @session.id))
58
58
  run
59
59
  rescue => error
@@ -129,15 +129,20 @@ class AgentLoop
129
129
  # @return [Array<Class<Tools::Base>>]
130
130
  STANDARD_TOOLS = [Tools::Bash, Tools::Read, Tools::Write, Tools::Edit, Tools::WebGet, Tools::Think, Tools::Remember].freeze
131
131
 
132
+ # Tools that bypass {Session#granted_tools} filtering.
133
+ # The agent's reasoning depends on these regardless of task scope.
134
+ # @return [Array<Class<Tools::Base>>]
135
+ ALWAYS_GRANTED_TOOLS = [Tools::Think].freeze
136
+
132
137
  # Name-to-class mapping for tool restriction validation and registry building.
133
138
  # @return [Hash{String => Class<Tools::Base>}]
134
139
  STANDARD_TOOLS_BY_NAME = STANDARD_TOOLS.index_by(&:tool_name).freeze
135
140
 
136
141
  private
137
142
 
138
- # @see Session#create_user_event
139
- def persist_user_event(content)
140
- @session.create_user_event(content)
143
+ # @see Session#create_user_message
144
+ def persist_user_message(content)
145
+ @session.create_user_message(content)
141
146
  end
142
147
 
143
148
  # Assembles LLM options (system prompt, environment context).
@@ -170,7 +175,7 @@ class AgentLoop
170
175
  unless @session.sub_agent?
171
176
  registry.register(Tools::SpawnSubagent)
172
177
  registry.register(Tools::SpawnSpecialist)
173
- registry.register(Tools::RequestFeature)
178
+ registry.register(Tools::OpenIssue)
174
179
  end
175
180
 
176
181
  register_mcp_tools(registry)
@@ -194,12 +199,15 @@ class AgentLoop
194
199
 
195
200
  # Standard tools available to this session.
196
201
  # Returns all when {Session#granted_tools} is nil (no restriction).
197
- # Returns only matching tools when granted_tools is an array.
202
+ # Returns only matching tools when granted_tools is an array,
203
+ # always including {ALWAYS_GRANTED_TOOLS}.
198
204
  #
199
205
  # @return [Array<Class<Tools::Base>>] tool classes to register
200
206
  def granted_standard_tools
201
- return STANDARD_TOOLS unless @session.granted_tools
207
+ granted = @session.granted_tools
208
+ return STANDARD_TOOLS unless granted
202
209
 
203
- @session.granted_tools.filter_map { |name| STANDARD_TOOLS_BY_NAME[name] }
210
+ explicitly_granted = granted.filter_map { |name| STANDARD_TOOLS_BY_NAME[name] }
211
+ (ALWAYS_GRANTED_TOOLS + explicitly_granted).uniq
204
212
  end
205
213
  end
@@ -37,7 +37,7 @@ module Agents
37
37
  # @return [self]
38
38
  def load_all
39
39
  load_directory(BUILTIN_DIR)
40
- load_directory(USER_DIR)
40
+ load_directory(USER_DIR) unless Rails.env.test?
41
41
  self
42
42
  end
43
43
 
@@ -27,7 +27,7 @@ module AnalyticalBrain
27
27
  ──────────────────────────────
28
28
  SESSION NAMING
29
29
  ──────────────────────────────
30
- Call rename_session when the topic becomes clear or shifts.
30
+ Name the session when the topic becomes clear. Rename if it shifts.
31
31
  Format: one emoji + 1-3 descriptive words.
32
32
  PROMPT
33
33
  tools: [Tools::RenameSession]
@@ -38,10 +38,10 @@ module AnalyticalBrain
38
38
  ──────────────────────────────
39
39
  SUB-AGENT NAMING
40
40
  ──────────────────────────────
41
- Call assign_nickname to give this sub-agent a short, memorable nickname.
41
+ Give this sub-agent a memorable nickname based on its task.
42
42
  Format: 1-3 lowercase words joined by hyphens (e.g. "loop-sleuth", "api-scout").
43
- Evocative of the task, fun, easy to type after @.
44
- Generate EXACTLY ONE nickname. If taken, pick another — no numeric suffixes.
43
+ Evocative, fun, easy to type after @.
44
+ One nickname per call. If taken, pick another — no numeric suffixes.
45
45
  PROMPT
46
46
  tools: [Tools::AssignNickname]
47
47
  ),
@@ -51,8 +51,8 @@ module AnalyticalBrain
51
51
  ──────────────────────────────
52
52
  SKILL MANAGEMENT
53
53
  ──────────────────────────────
54
- Call activate_skill when the conversation matches a skill's description.
55
- Call deactivate_skill when the agent moves to a different domain.
54
+ Activate skills when the conversation enters their domain.
55
+ Deactivate when the agent moves to a different domain.
56
56
  Multiple skills can be active at once.
57
57
  PROMPT
58
58
  tools: [Tools::ActivateSkill, Tools::DeactivateSkill]
@@ -63,11 +63,11 @@ module AnalyticalBrain
63
63
  ──────────────────────────────
64
64
  WORKFLOW MANAGEMENT
65
65
  ──────────────────────────────
66
- Call read_workflow when the user starts a multi-step task matching a workflow description.
67
- Read the returned content and use judgment to create appropriate goals — not a mechanical 1:1 mapping.
66
+ Activate a workflow when the user starts a multi-step task that matches one.
67
+ Read the returned content and use judgment to create goals — not a mechanical 1:1 mapping.
68
68
  Adapt to context: skip irrelevant steps, add extra steps for unfamiliar areas.
69
- Call deactivate_workflow when the workflow completes or the user shifts focus.
70
- Only one workflow can be active at a time — activating a new one replaces the previous.
69
+ Deactivate the workflow when it completes or the user shifts focus.
70
+ Only one workflow active at a time — activating a new one replaces the previous.
71
71
  PROMPT
72
72
  tools: [Tools::ReadWorkflow, Tools::DeactivateWorkflow]
73
73
  ),
@@ -77,11 +77,11 @@ module AnalyticalBrain
77
77
  ──────────────────────────────
78
78
  GOAL TRACKING
79
79
  ──────────────────────────────
80
- Call set_goal to create a root goal when the user starts a multi-step task.
81
- Call set_goal with parent_goal_id to add sub-goals (TODO items) under it.
82
- Call update_goal to refine a goal's description as understanding evolves.
83
- Call finish_goal when the main agent completes work a goal describes.
84
- Finishing a root goal cascades — all active sub-goals are completed too.
80
+ Create a root goal when the user starts a multi-step task.
81
+ Break it into sub-goals as the plan becomes clear.
82
+ Refine goal wording as understanding evolves.
83
+ Mark goals complete when the agent finishes the work they describe.
84
+ Completing a root goal cascades — all sub-goals are finished too.
85
85
  Never duplicate an existing goal — check the active goals list first.
86
86
  PROMPT
87
87
  tools: [Tools::SetGoal, Tools::UpdateGoal, Tools::FinishGoal]
@@ -89,17 +89,17 @@ module AnalyticalBrain
89
89
  }.freeze
90
90
 
91
91
  BASE_PROMPT = <<~PROMPT
92
- You are a background automation that manages session metadata.
93
- You MUST ONLY communicate through tool calls NEVER output text.
94
- Always finish by calling everything_is_ready.
92
+ You manage context for the main agent — skills, goals, workflows, and session names.
93
+ Watch the conversation and act when context needs updating.
94
+ Communicate only through tool calls — never output text.
95
95
  PROMPT
96
96
 
97
97
  COMPLETION_PROMPT = <<~PROMPT
98
98
  ──────────────────────────────
99
99
  COMPLETION
100
100
  ──────────────────────────────
101
- Call everything_is_ready as your LAST tool call, every time.
102
- If nothing needs changing, call it immediately as your only tool call.
101
+ Always finish with everything_is_ready.
102
+ If nothing needs attention, call it immediately.
103
103
  PROMPT
104
104
 
105
105
  # Which responsibilities activate for each session type.
@@ -118,7 +118,7 @@ module AnalyticalBrain
118
118
  end
119
119
 
120
120
  # Runs the analytical brain loop. Builds context from the session's
121
- # recent events, calls the LLM with the session-appropriate tool set,
121
+ # recent messages, calls the LLM with the session-appropriate tool set,
122
122
  # and executes any tool calls against the session.
123
123
  #
124
124
  # Events emitted during tool execution are not persisted — the phantom
@@ -130,12 +130,12 @@ module AnalyticalBrain
130
130
  messages = build_messages
131
131
  sid = @session.id
132
132
  if messages.empty?
133
- log.debug("session=#{sid} — no events, skipping")
133
+ log.debug("session=#{sid} — no messages, skipping")
134
134
  return
135
135
  end
136
136
 
137
137
  system = build_system_prompt
138
- log.info("session=#{sid} — running (#{recent_events.size} events)")
138
+ log.info("session=#{sid} — running (#{recent_messages.size} messages)")
139
139
  log.debug("system prompt:\n#{system}")
140
140
  log.debug("user message:\n#{messages.first[:content]}")
141
141
 
@@ -162,18 +162,18 @@ module AnalyticalBrain
162
162
  active_responsibility_keys.map { |key| RESPONSIBILITIES.fetch(key) }
163
163
  end
164
164
 
165
- # Builds a condensed transcript of recent events as a single user message.
165
+ # Builds a condensed transcript of recent messages as a single user message.
166
166
  # The framing differs by session type:
167
167
  #
168
168
  # * **Parent:** "The main session is working on this: [transcript]"
169
169
  # * **Child:** "A sub-agent has been spawned with this task: [transcript]"
170
170
  #
171
- # @return [Array<Hash>] single-element messages array, or empty if no events
171
+ # @return [Array<Hash>] single-element messages array, or empty if no messages
172
172
  def build_messages
173
- events = recent_events
174
- return [] if events.empty?
173
+ messages = recent_messages
174
+ return [] if messages.empty?
175
175
 
176
- transcript = events.filter_map { |event| EventDecorator.for(event)&.render("brain") }.join("\n")
176
+ transcript = messages.filter_map { |msg| MessageDecorator.for(msg)&.render("brain") }.join("\n")
177
177
 
178
178
  if @session.sub_agent?
179
179
  build_child_message(transcript)
@@ -189,7 +189,7 @@ module AnalyticalBrain
189
189
  #{transcript}
190
190
  ```
191
191
 
192
- Observe the conversation and take action: manage goals, activate or deactivate relevant skills, read workflows when a multi-step task matches, rename the session if needed, then call everything_is_ready.
192
+ Review and take any needed actions, then call everything_is_ready.
193
193
  MSG
194
194
  [{role: "user", content: content}]
195
195
  end
@@ -201,17 +201,17 @@ module AnalyticalBrain
201
201
  #{transcript}
202
202
  ```
203
203
 
204
- Assign a memorable nickname based on the task, activate relevant skills, then call everything_is_ready.
204
+ Assign a nickname, activate relevant skills, then call everything_is_ready.
205
205
  MSG
206
206
  [{role: "user", content: content}]
207
207
  end
208
208
 
209
- # @return [Array<Event>] most recent events in chronological order
210
- def recent_events
211
- @session.events
212
- .context_events
209
+ # @return [Array<Message>] most recent messages in chronological order
210
+ def recent_messages
211
+ @session.messages
212
+ .context_messages
213
213
  .reorder(id: :desc)
214
- .limit(Anima::Settings.analytical_brain_event_window)
214
+ .limit(Anima::Settings.analytical_brain_message_window)
215
215
  .to_a
216
216
  .reverse
217
217
  end
@@ -8,19 +8,15 @@ module AnalyticalBrain
8
8
  class ActivateSkill < ::Tools::Base
9
9
  def self.tool_name = "activate_skill"
10
10
 
11
- def self.description = "Activate a domain knowledge skill on the main session. " \
12
- "The skill's content will be injected into the agent's system prompt."
11
+ def self.description = "Give the agent domain knowledge relevant to the current conversation."
13
12
 
14
13
  def self.input_schema
15
14
  {
16
15
  type: "object",
17
16
  properties: {
18
- name: {
19
- type: "string",
20
- description: "Name of the skill to activate (from the available skills list)"
21
- }
17
+ skill_name: {type: "string"}
22
18
  },
23
- required: %w[name]
19
+ required: %w[skill_name]
24
20
  }
25
21
  end
26
22
 
@@ -29,11 +25,11 @@ module AnalyticalBrain
29
25
  @main_session = main_session
30
26
  end
31
27
 
32
- # @param input [Hash<String, Object>] with "name" key
28
+ # @param input [Hash<String, Object>] with "skill_name" key
33
29
  # @return [String] confirmation message with skill description
34
30
  # @return [Hash] with :error key on validation failure
35
31
  def execute(input)
36
- skill_name = input["name"].to_s.strip
32
+ skill_name = input["skill_name"].to_s.strip
37
33
  return {error: "Skill name cannot be blank"} if skill_name.empty?
38
34
 
39
35
  skill = @main_session.activate_skill(skill_name)
@@ -17,8 +17,7 @@ module AnalyticalBrain
17
17
 
18
18
  def self.tool_name = "assign_nickname"
19
19
 
20
- def self.description = "Assign a short, memorable nickname to this sub-agent. " \
21
- "The nickname is permanent — it will not change."
20
+ def self.description = "Assign a permanent nickname to this sub-agent."
22
21
 
23
22
  def self.input_schema
24
23
  {
@@ -26,8 +25,7 @@ module AnalyticalBrain
26
25
  properties: {
27
26
  nickname: {
28
27
  type: "string",
29
- description: "1-3 lowercase words joined by hyphens (e.g. 'loop-sleuth', 'api-scout'). " \
30
- "Evocative of the task, fun, easy to type after @."
28
+ description: "Lowercase, hyphenated (e.g. 'loop-sleuth')."
31
29
  }
32
30
  },
33
31
  required: %w[nickname]
@@ -7,19 +7,15 @@ module AnalyticalBrain
7
7
  class DeactivateSkill < ::Tools::Base
8
8
  def self.tool_name = "deactivate_skill"
9
9
 
10
- def self.description = "Deactivate a skill that is no longer relevant. " \
11
- "The skill's content will be removed from the agent's system prompt."
10
+ def self.description = "Remove domain knowledge that is no longer relevant."
12
11
 
13
12
  def self.input_schema
14
13
  {
15
14
  type: "object",
16
15
  properties: {
17
- name: {
18
- type: "string",
19
- description: "Name of the skill to deactivate (from the currently active skills list)"
20
- }
16
+ skill_name: {type: "string"}
21
17
  },
22
- required: %w[name]
18
+ required: %w[skill_name]
23
19
  }
24
20
  end
25
21
 
@@ -28,11 +24,11 @@ module AnalyticalBrain
28
24
  @main_session = main_session
29
25
  end
30
26
 
31
- # @param input [Hash<String, Object>] with "name" key
27
+ # @param input [Hash<String, Object>] with "skill_name" key
32
28
  # @return [String] confirmation message
33
29
  # @return [Hash] with :error key on validation failure
34
30
  def execute(input)
35
- skill_name = input["name"].to_s.strip
31
+ skill_name = input["skill_name"].to_s.strip
36
32
  return {error: "Skill name cannot be blank"} if skill_name.empty?
37
33
 
38
34
  @main_session.deactivate_skill(skill_name)
@@ -11,8 +11,7 @@ module AnalyticalBrain
11
11
  class EverythingIsReady < ::Tools::Base
12
12
  def self.tool_name = "everything_is_ready"
13
13
 
14
- def self.description = "Signal that no changes are needed. " \
15
- "Call this when the session name and active skills are already appropriate."
14
+ def self.description = "Nothing else to do."
16
15
 
17
16
  def self.input_schema
18
17
  {type: "object", properties: {}, required: []}
@@ -7,17 +7,13 @@ module AnalyticalBrain
7
7
  class FinishGoal < ::Tools::Base
8
8
  def self.tool_name = "finish_goal"
9
9
 
10
- def self.description = "Mark a goal as completed. " \
11
- "Use this when the main agent has finished the work described by the goal."
10
+ def self.description = "Mark a goal as completed."
12
11
 
13
12
  def self.input_schema
14
13
  {
15
14
  type: "object",
16
15
  properties: {
17
- goal_id: {
18
- type: "integer",
19
- description: "ID of the goal to mark as completed"
20
- }
16
+ goal_id: {type: "integer"}
21
17
  },
22
18
  required: %w[goal_id]
23
19
  }
@@ -49,7 +45,8 @@ module AnalyticalBrain
49
45
  # brain learns to check status before retrying.
50
46
  def complete(goal)
51
47
  id = goal.id
52
- return {error: "Goal already completed: #{goal.description} (id: #{id})"} if goal.completed?
48
+ desc = goal.description
49
+ return {error: "Goal already completed: #{desc} (id: #{id})"} if goal.completed?
53
50
 
54
51
  released = 0
55
52
  Goal.transaction do
@@ -58,7 +55,7 @@ module AnalyticalBrain
58
55
  released = goal.release_orphaned_pins!
59
56
  end
60
57
 
61
- msg = "Goal completed: #{goal.description} (id: #{id})"
58
+ msg = "Goal completed: #{desc} (id: #{id})"
62
59
  msg += " (released #{released} orphaned pins)" if released > 0
63
60
  msg
64
61
  end