anima-core 1.4.0 → 1.5.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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +18 -20
  3. data/README.md +61 -95
  4. data/agents/thoughts-analyzer.md +12 -7
  5. data/anima-core.gemspec +1 -0
  6. data/app/channels/session_channel.rb +38 -58
  7. data/app/decorators/agent_message_decorator.rb +7 -2
  8. data/app/decorators/message_decorator.rb +31 -100
  9. data/app/decorators/pending_from_melete_decorator.rb +36 -0
  10. data/app/decorators/pending_from_melete_goal_decorator.rb +13 -0
  11. data/app/decorators/pending_from_melete_skill_decorator.rb +19 -0
  12. data/app/decorators/pending_from_melete_workflow_decorator.rb +13 -0
  13. data/app/decorators/pending_from_mneme_decorator.rb +44 -0
  14. data/app/decorators/pending_message_decorator.rb +94 -0
  15. data/app/decorators/pending_subagent_decorator.rb +46 -0
  16. data/app/decorators/pending_tool_response_decorator.rb +51 -0
  17. data/app/decorators/pending_user_message_decorator.rb +22 -0
  18. data/app/decorators/system_message_decorator.rb +5 -0
  19. data/app/decorators/tool_call_decorator.rb +13 -2
  20. data/app/decorators/tool_response_decorator.rb +2 -2
  21. data/app/decorators/user_message_decorator.rb +7 -2
  22. data/app/jobs/count_tokens_job.rb +23 -0
  23. data/app/jobs/drain_job.rb +169 -0
  24. data/app/jobs/melete_enrichment_job/goal_change_listener.rb +52 -0
  25. data/app/jobs/melete_enrichment_job.rb +48 -0
  26. data/app/jobs/mneme_enrichment_job.rb +46 -0
  27. data/app/jobs/tool_execution_job.rb +87 -0
  28. data/app/models/concerns/token_estimation.rb +54 -0
  29. data/app/models/goal.rb +21 -10
  30. data/app/models/message.rb +47 -36
  31. data/app/models/pending_message.rb +276 -29
  32. data/app/models/pinned_message.rb +8 -3
  33. data/app/models/session.rb +468 -432
  34. data/app/models/snapshot.rb +11 -21
  35. data/bin/inspect-cassette +17 -4
  36. data/config/application.rb +1 -0
  37. data/config/initializers/event_subscribers.rb +71 -4
  38. data/config/initializers/inflections.rb +3 -1
  39. data/db/cable_structure.sql +3 -3
  40. data/db/migrate/20260407170803_remove_viewport_message_ids_from_sessions.rb +5 -0
  41. data/db/migrate/20260407180400_remove_mneme_snapshot_pointer_columns_from_sessions.rb +6 -0
  42. data/db/migrate/20260411120553_add_token_count_to_pinned_messages.rb +5 -0
  43. data/db/migrate/20260411172926_remove_active_skills_and_workflow_from_sessions.rb +6 -0
  44. data/db/migrate/20260412110625_replace_processing_with_aasm_state.rb +6 -0
  45. data/db/migrate/20260418150323_add_kind_and_message_type_to_pending_messages.rb +6 -0
  46. data/db/migrate/20260419120000_add_drain_fields_to_pending_messages.rb +7 -0
  47. data/db/migrate/20260419130000_drop_pending_messages_kind_default.rb +5 -0
  48. data/db/migrate/20260419140000_add_drain_indexes_to_pending_messages.rb +8 -0
  49. data/db/migrate/20260420100000_add_hud_visibility_to_sessions.rb +15 -0
  50. data/db/queue_structure.sql +13 -13
  51. data/db/structure.sql +44 -31
  52. data/lib/agents/registry.rb +1 -1
  53. data/lib/anima/settings.rb +7 -33
  54. data/lib/anima/version.rb +1 -1
  55. data/lib/events/authentication_required.rb +24 -0
  56. data/lib/events/bounce_back.rb +4 -4
  57. data/lib/events/eviction_completed.rb +28 -0
  58. data/lib/events/goal_created.rb +28 -0
  59. data/lib/events/goal_updated.rb +32 -0
  60. data/lib/events/llm_responded.rb +35 -0
  61. data/lib/events/message_created.rb +27 -0
  62. data/lib/events/message_updated.rb +25 -0
  63. data/lib/events/session_state_changed.rb +30 -0
  64. data/lib/events/skill_activated.rb +28 -0
  65. data/lib/events/start_melete.rb +36 -0
  66. data/lib/events/start_mneme.rb +33 -0
  67. data/lib/events/start_processing.rb +32 -0
  68. data/lib/events/subagent_evicted.rb +31 -0
  69. data/lib/events/subscribers/active_state_broadcaster.rb +27 -0
  70. data/lib/events/subscribers/authentication_broadcaster.rb +34 -0
  71. data/lib/events/subscribers/drain_kickoff.rb +20 -0
  72. data/lib/events/subscribers/eviction_broadcaster.rb +26 -0
  73. data/lib/events/subscribers/llm_response_handler.rb +111 -0
  74. data/lib/events/subscribers/melete_kickoff.rb +24 -0
  75. data/lib/events/subscribers/message_broadcaster.rb +34 -0
  76. data/lib/events/subscribers/mneme_kickoff.rb +24 -0
  77. data/lib/events/subscribers/mneme_scheduler.rb +21 -0
  78. data/lib/events/subscribers/persister.rb +6 -8
  79. data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
  80. data/lib/events/subscribers/subagent_message_router.rb +26 -29
  81. data/lib/events/subscribers/subagent_visibility_broadcaster.rb +33 -0
  82. data/lib/events/subscribers/tool_response_creator.rb +33 -0
  83. data/lib/events/subscribers/transient_broadcaster.rb +1 -1
  84. data/lib/events/tool_executed.rb +34 -0
  85. data/lib/events/workflow_activated.rb +27 -0
  86. data/lib/llm/client.rb +41 -201
  87. data/lib/mcp/client_manager.rb +41 -46
  88. data/lib/mcp/stdio_transport.rb +9 -5
  89. data/lib/{analytical_brain → melete}/runner.rb +63 -68
  90. data/lib/{analytical_brain → melete}/tools/activate_skill.rb +1 -1
  91. data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +2 -2
  92. data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
  93. data/lib/{analytical_brain → melete}/tools/finish_goal.rb +3 -3
  94. data/lib/{analytical_brain → melete}/tools/goal_messaging.rb +4 -3
  95. data/lib/{analytical_brain → melete}/tools/read_workflow.rb +2 -2
  96. data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
  97. data/lib/{analytical_brain → melete}/tools/set_goal.rb +1 -1
  98. data/lib/{analytical_brain → melete}/tools/update_goal.rb +4 -4
  99. data/lib/{analytical_brain.rb → melete.rb} +6 -3
  100. data/lib/mneme/base_runner.rb +121 -0
  101. data/lib/mneme/l2_runner.rb +14 -20
  102. data/lib/mneme/recall_runner.rb +132 -0
  103. data/lib/mneme/runner.rb +118 -171
  104. data/lib/mneme/search.rb +104 -62
  105. data/lib/mneme/tools/nothing_to_surface.rb +25 -0
  106. data/lib/mneme/tools/save_snapshot.rb +2 -10
  107. data/lib/mneme/tools/surface_memory.rb +89 -0
  108. data/lib/mneme.rb +11 -5
  109. data/lib/shell_session.rb +287 -612
  110. data/lib/skills/definition.rb +2 -2
  111. data/lib/skills/registry.rb +1 -1
  112. data/lib/tools/base.rb +16 -0
  113. data/lib/tools/bash.rb +25 -57
  114. data/lib/tools/edit.rb +2 -0
  115. data/lib/tools/read.rb +2 -0
  116. data/lib/tools/registry.rb +79 -3
  117. data/lib/tools/{recall.rb → search_messages.rb} +19 -21
  118. data/lib/tools/spawn_specialist.rb +16 -10
  119. data/lib/tools/spawn_subagent.rb +20 -14
  120. data/lib/tools/subagent_prompts.rb +4 -4
  121. data/lib/tools/think.rb +1 -1
  122. data/lib/tools/{remember.rb → view_messages.rb} +10 -10
  123. data/lib/tools/write.rb +2 -0
  124. data/lib/tui/app.rb +5 -4
  125. data/lib/tui/braille_spinner.rb +7 -7
  126. data/lib/tui/decorators/base_decorator.rb +24 -3
  127. data/lib/tui/message_store.rb +93 -44
  128. data/lib/tui/screens/chat.rb +94 -20
  129. data/lib/tui/settings.rb +9 -2
  130. data/lib/workflows/definition.rb +3 -3
  131. data/lib/workflows/registry.rb +1 -1
  132. data/skills/github.md +38 -0
  133. data/templates/config.toml +4 -23
  134. data/workflows/review_pr.md +18 -14
  135. metadata +86 -28
  136. data/app/jobs/agent_request_job.rb +0 -199
  137. data/app/jobs/analytical_brain_job.rb +0 -33
  138. data/app/jobs/count_message_tokens_job.rb +0 -39
  139. data/app/jobs/passive_recall_job.rb +0 -24
  140. data/app/models/concerns/message/broadcasting.rb +0 -86
  141. data/lib/agent_loop.rb +0 -215
  142. data/lib/analytical_brain/tools/deactivate_skill.rb +0 -40
  143. data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -35
  144. data/lib/events/agent_message.rb +0 -25
  145. data/lib/events/subscribers/message_collector.rb +0 -64
  146. data/lib/events/tool_call.rb +0 -31
  147. data/lib/events/tool_response.rb +0 -33
  148. data/lib/mneme/compressed_viewport.rb +0 -204
  149. data/lib/mneme/passive_recall.rb +0 -138
@@ -1,26 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
4
- # Orchestrates the analytical brain — a phantom (non-persisted) LLM loop
5
- # that observes a session and performs background maintenance via tools.
3
+ module Melete
4
+ # Orchestrates Melete — a phantom (non-persisted) LLM loop that
5
+ # observes the main session and prepares skills, workflows, goals,
6
+ # and session names so the main agent can perform cleanly.
6
7
  #
7
- # The brain's capabilities are assembled from independent {Responsibility}
8
- # modules, each contributing a prompt section and tools. Which modules are
9
- # active depends on the session type:
8
+ # Melete's capabilities are assembled from independent {Responsibility}
9
+ # modules, each contributing a prompt section and tools. Which modules
10
+ # are active depends on the session type:
10
11
  #
11
12
  # * **Parent sessions** — session naming, skill/workflow/goal management
12
13
  # * **Child sessions** — sub-agent nickname assignment, skill management
13
- # (goal tracking and workflows disabled — sub-agents manage their sole goal
14
- # via mark_goal_completed)
14
+ # (goal tracking and workflows disabled — sub-agents manage their sole
15
+ # goal via mark_goal_completed)
15
16
  #
16
- # Tools mutate the observed session directly (e.g. renaming it, activating
17
- # skills), but no trace of the brain's reasoning is persisted — events are
18
- # emitted into a phantom session (session_id: nil).
17
+ # Tools mutate the observed session directly (e.g. renaming it,
18
+ # activating skills), but no trace of Melete's reasoning is persisted —
19
+ # events are emitted into a phantom session (session_id: nil).
19
20
  #
20
21
  # @example
21
- # AnalyticalBrain::Runner.new(session).call
22
+ # Melete::Runner.new(session).call
22
23
  class Runner
23
- # A composable unit of brain capability: a prompt section + its tools.
24
+ # A composable unit of Melete's capability: a prompt section + its tools.
24
25
  Responsibility = Data.define(:prompt, :tools)
25
26
 
26
27
  RESPONSIBILITIES = {
@@ -29,7 +30,7 @@ module AnalyticalBrain
29
30
  ──────────────────────────────
30
31
  SESSION NAMING
31
32
  ──────────────────────────────
32
- Name the session when the topic becomes clear. Rename if it shifts.
33
+ Name the session once the topic becomes clear. Rename if it shifts.
33
34
  Format: one emoji + 1-3 descriptive words.
34
35
  PROMPT
35
36
  tools: [Tools::RenameSession]
@@ -53,12 +54,11 @@ module AnalyticalBrain
53
54
  ──────────────────────────────
54
55
  SKILL MANAGEMENT
55
56
  ──────────────────────────────
56
- Activate skills when the conversation signals intent — before the agent acts on it.
57
- Late activation means the agent works without domain knowledge.
58
- Deactivate when the agent moves to a different domain.
59
- Multiple skills can be active at once.
57
+ Activate a skill the moment the conversation signals its domain — before Aoide needs it. Late activation means she's working without the knowledge you prepared.
58
+
59
+ An activated skill rides Aoide's viewport as a message and leaves on its own when it evicts you cannot take it back. So be careful: an irrelevant skill crowds her context with text she has to read and ignore until it falls off. Match each activation to the work actually in front of her. Multiple skills can be active at once — each one is a page she has to carry until it evicts.
60
60
  PROMPT
61
- tools: [Tools::ActivateSkill, Tools::DeactivateSkill]
61
+ tools: [Tools::ActivateSkill]
62
62
  ),
63
63
 
64
64
  workflow_management: Responsibility.new(
@@ -66,13 +66,11 @@ module AnalyticalBrain
66
66
  ──────────────────────────────
67
67
  WORKFLOW MANAGEMENT
68
68
  ──────────────────────────────
69
- Activate a workflow when the user starts a multi-step task that matches one.
70
- Read the returned content and use judgment to create goals — not a mechanical 1:1 mapping.
71
- Adapt to context: skip irrelevant steps, add extra steps for unfamiliar areas.
72
- Deactivate the workflow when it completes or the user shifts focus.
73
- Only one workflow active at a time — activating a new one replaces the previous.
69
+ Activate a workflow when Aoide starts a multi-step task that matches one. Read the returned content and use judgment to turn it into goals — not a mechanical 1:1 mapping. Adapt: skip irrelevant steps, add extra ones for unfamiliar ground.
70
+
71
+ Like skills, a workflow rides Aoide's viewport once activated and leaves when it evicts — there is no deactivation. An irrelevant or stale workflow is text Aoide carries whether she needs it or not, so only activate one when the task genuinely matches.
74
72
  PROMPT
75
- tools: [Tools::ReadWorkflow, Tools::DeactivateWorkflow]
73
+ tools: [Tools::ReadWorkflow]
76
74
  ),
77
75
 
78
76
  goal_tracking: Responsibility.new(
@@ -80,29 +78,25 @@ module AnalyticalBrain
80
78
  ──────────────────────────────
81
79
  GOAL TRACKING
82
80
  ──────────────────────────────
83
- Create a root goal when the user starts a multi-step task.
84
- Break it into sub-goals as the plan becomes clear.
85
- Refine goal wording as understanding evolves.
86
- Mark goals complete when the agent finishes the work they describe.
87
- Completing a root goal cascades — all sub-goals are finished too.
88
- Never duplicate an existing goal — check the active goals list first.
81
+ Create a root goal when Aoide starts a multi-step task. Break it into sub-goals as the plan takes shape. Refine wording as understanding evolves. Mark goals complete when she finishes the work they describe — completing a root cascades through its sub-goals.
82
+
83
+ Check the active goals list before every set_goal call. Never duplicate an existing goal a duplicate wastes a slot and blurs which version Aoide should track.
89
84
  PROMPT
90
85
  tools: [Tools::SetGoal, Tools::UpdateGoal, Tools::FinishGoal]
91
86
  )
92
87
  }.freeze
93
88
 
94
89
  BASE_PROMPT = <<~PROMPT
95
- You manage context for the main agentskills, goals, workflows, and session names.
96
- Watch the conversation and act when context needs updating.
97
- Communicate only through tool calls — never output text.
90
+ You are Melete, the muse of practice. You share the conversation with two sisters Aoide, who speaks and performs, and Mneme, who holds memory. Your work is preparation: when Aoide speaks, she should have the skills she needs, the workflow in front of her, and a clear sense of what she's working toward.
91
+
92
+ Act only through tool calls. Never output text your contribution is the scene you set, not the words you say.
98
93
  PROMPT
99
94
 
100
95
  COMPLETION_PROMPT = <<~PROMPT
101
96
  ──────────────────────────────
102
97
  COMPLETION
103
98
  ──────────────────────────────
104
- Always finish with everything_is_ready.
105
- If nothing needs attention, call it immediately.
99
+ Finish every run with everything_is_ready. If nothing needs your attention, call it immediately.
106
100
  PROMPT
107
101
 
108
102
  # Which responsibilities activate for each session type.
@@ -115,12 +109,12 @@ module AnalyticalBrain
115
109
  @session = session
116
110
  @client = client || LLM::Client.new(
117
111
  model: Anima::Settings.fast_model,
118
- max_tokens: Anima::Settings.analytical_brain_max_tokens,
119
- logger: AnalyticalBrain.logger
112
+ max_tokens: Anima::Settings.melete_max_tokens,
113
+ logger: Melete.logger
120
114
  )
121
115
  end
122
116
 
123
- # Runs the analytical brain loop. Builds context from the session's
117
+ # Runs Melete's loop. Builds context from the session's
124
118
  # recent messages, calls the LLM with the session-appropriate tool set,
125
119
  # and executes any tool calls against the session.
126
120
  #
@@ -132,20 +126,15 @@ module AnalyticalBrain
132
126
  def call
133
127
  messages = build_messages
134
128
  sid = @session.id
135
- if messages.empty?
136
- log.debug("session=#{sid} — no messages, skipping")
137
- return
138
- end
139
129
 
140
130
  system = build_system_prompt
141
- log.info("session=#{sid} — running (#{recent_messages.size} messages)")
131
+ log.info("session=#{sid} — running (#{recent_messages.size} messages + #{pending_messages.size} pending)")
142
132
  log.debug("system prompt:\n#{system}")
143
133
  log.debug("user message:\n#{messages.first[:content]}")
144
134
 
145
135
  result = @client.chat_with_tools(
146
136
  messages,
147
137
  registry: build_registry,
148
- session_id: nil,
149
138
  system: system
150
139
  )
151
140
 
@@ -171,12 +160,11 @@ module AnalyticalBrain
171
160
  # * **Parent:** "The main session is working on this: [transcript]"
172
161
  # * **Child:** "A sub-agent has been spawned with this task: [transcript]"
173
162
  #
174
- # @return [Array<Hash>] single-element messages array, or empty if no messages
163
+ # @return [Array<Hash>] single-element messages array
175
164
  def build_messages
176
- messages = recent_messages
177
- return [] if messages.empty?
178
-
179
- transcript = messages.filter_map { |msg| MessageDecorator.for(msg)&.render("brain") }.join("\n")
165
+ transcript = (recent_messages + pending_messages)
166
+ .filter_map { |entry| entry.decorate.render("melete") }
167
+ .join("\n")
180
168
 
181
169
  if @session.sub_agent?
182
170
  build_child_message(transcript)
@@ -187,12 +175,12 @@ module AnalyticalBrain
187
175
 
188
176
  def build_parent_message(transcript)
189
177
  content = <<~MSG.strip
190
- The main session is working on this:
178
+ Aoide is working on this:
191
179
  ```
192
180
  #{transcript}
193
181
  ```
194
182
 
195
- Review and take any needed actions, then call everything_is_ready.
183
+ Prepare whatever she needs for the next exchange, then call everything_is_ready.
196
184
  MSG
197
185
  [{role: "user", content: content}]
198
186
  end
@@ -204,7 +192,7 @@ module AnalyticalBrain
204
192
  #{transcript}
205
193
  ```
206
194
 
207
- Assign a nickname and activate relevant skills, then call everything_is_ready.
195
+ Give the sub-agent a nickname and activate the skills she'll need, then call everything_is_ready.
208
196
  MSG
209
197
  [{role: "user", content: content}]
210
198
  end
@@ -212,13 +200,20 @@ module AnalyticalBrain
212
200
  # @return [Array<Message>] most recent messages in chronological order
213
201
  def recent_messages
214
202
  @session.messages
215
- .context_messages
216
203
  .reorder(id: :desc)
217
- .limit(Anima::Settings.analytical_brain_message_window)
204
+ .limit(Anima::Settings.melete_message_window)
218
205
  .to_a
219
206
  .reverse
220
207
  end
221
208
 
209
+ # @return [Array<PendingMessage>] everything currently queued for the next
210
+ # drain cycle — the trigger user message, Mneme's recalls, earlier
211
+ # enrichment output. Appended after real messages because they are
212
+ # the "future" Melete is preparing for.
213
+ def pending_messages
214
+ @session.pending_messages.order(:created_at).to_a
215
+ end
216
+
222
217
  # Builds the system prompt from active responsibilities + context sections.
223
218
  #
224
219
  # @return [String]
@@ -251,7 +246,7 @@ module AnalyticalBrain
251
246
  SECTION
252
247
  end
253
248
 
254
- # Shows sibling nicknames already in use so the brain avoids collisions
249
+ # Shows sibling nicknames already in use so Melete avoids collisions
255
250
  # at prompt level (the tool also validates at execution time).
256
251
  #
257
252
  # @return [String, nil] sibling names section, or nil for parent sessions
@@ -273,11 +268,11 @@ module AnalyticalBrain
273
268
  end
274
269
 
275
270
  # Skills already visible in the viewport are excluded from the catalog
276
- # so the brain doesn't re-activate them. When a skill evicts from the
277
- # viewport, it reappears here and the brain can re-inject if relevant.
271
+ # so Melete doesn't re-activate them. When a skill evicts from the
272
+ # viewport, it reappears here and she can re-inject if relevant.
278
273
  #
279
274
  # @see Session#skills_in_viewport
280
- # @return [String] available skills list for the analytical brain
275
+ # @return [String] available skills list for Melete
281
276
  def skills_catalog_section
282
277
  present = @session.skills_in_viewport
283
278
  catalog = Skills::Registry.instance.catalog.except(*present)
@@ -297,7 +292,7 @@ module AnalyticalBrain
297
292
  # Workflows already visible in the viewport are excluded from the catalog.
298
293
  #
299
294
  # @see Session#workflow_in_viewport
300
- # @return [String] available workflows list for the analytical brain
295
+ # @return [String] available workflows list for Melete
301
296
  def workflows_catalog_section
302
297
  present = @session.workflow_in_viewport
303
298
  catalog = Workflows::Registry.instance.catalog.reject { |name, _| name == present }
@@ -314,13 +309,13 @@ module AnalyticalBrain
314
309
  SECTION
315
310
  end
316
311
 
317
- # @return [String, nil] active goals for the brain's own context,
318
- # so it knows what already exists and avoids duplicating
312
+ # @return [String, nil] active goals for Melete's own context,
313
+ # so she knows what already exists and avoids duplicating
319
314
  def active_goals_section
320
315
  root_goals = @session.goals.root.includes(:sub_goals).active.order(:created_at)
321
316
  return if root_goals.empty?
322
317
 
323
- lines = root_goals.map { |goal| format_goal_for_brain(goal) }
318
+ lines = root_goals.map { |goal| format_goal_for_melete(goal) }
324
319
  <<~SECTION
325
320
  ──────────────────────────────
326
321
  ACTIVE GOALS
@@ -330,14 +325,14 @@ module AnalyticalBrain
330
325
  end
331
326
 
332
327
  # Formats a root goal and its sub-goals as a markdown checklist
333
- # with IDs so the brain can reference them in finish_goal calls.
328
+ # with IDs so Melete can reference them in finish_goal calls.
334
329
  #
335
330
  # @example
336
331
  # "- Implement feature X (id: 42)\n - [x] Read code (id: 43)\n - [ ] Write tests (id: 44)"
337
332
  #
338
333
  # @param goal [Goal] root goal with preloaded sub_goals
339
- # @return [String] goal formatted as markdown checklist for brain context
340
- def format_goal_for_brain(goal)
334
+ # @return [String] goal formatted as markdown checklist for Melete's context
335
+ def format_goal_for_melete(goal)
341
336
  parts = ["- #{goal.description} (id: #{goal.id})"]
342
337
  goal.sub_goals.sort_by(&:created_at).each do |sub|
343
338
  checkbox = (sub.status == "completed") ? "[x]" : "[ ]"
@@ -346,8 +341,8 @@ module AnalyticalBrain
346
341
  parts.join("\n")
347
342
  end
348
343
 
349
- # @return [Logger] dev-only analytical brain logger
350
- def log = AnalyticalBrain.logger
344
+ # @return [Logger] dev-only Melete logger
345
+ def log = Melete.logger
351
346
 
352
347
  # @return [Tools::Registry] registry with tools from active responsibilities
353
348
  def build_registry
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Activates a domain knowledge skill on the main session.
6
6
  # The skill's content enters the conversation as a phantom
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Assigns a static nickname to a sub-agent session.
6
6
  # Operates on the session passed through the registry context.
@@ -9,7 +9,7 @@ module AnalyticalBrain
9
9
  # an error on collision so the LLM can pick another name naturally,
10
10
  # without programmatic suffixes.
11
11
  #
12
- # @see AnalyticalBrain::Runner — invokes this tool for child sessions
12
+ # @see Melete::Runner — invokes this tool for child sessions
13
13
  class AssignNickname < ::Tools::Base
14
14
  # Lowercase hyphenated words: "loop-sleuth", "api-scout", "test-fixer"
15
15
  NICKNAME_PATTERN = /\A[a-z][a-z0-9]*(-[a-z0-9]+)*\z/
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
- # Terminal tool that signals the analytical brain has completed its work.
5
+ # Terminal tool that signals Melete has completed its work.
6
6
  # Call this when no changes are needed — the current session state is
7
7
  # already good.
8
8
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Marks a goal as completed on the main session. Sets the status to
6
6
  # "completed" and records the completion timestamp.
@@ -43,8 +43,8 @@ module AnalyticalBrain
43
43
  # active sub-goals within a single transaction so the after_commit
44
44
  # broadcast includes the fully cascaded state.
45
45
  #
46
- # Returns an error for already-completed goals so the analytical
47
- # brain learns to check status before retrying.
46
+ # Returns an error for already-completed goals so Melete
47
+ # learns to check status before retrying.
48
48
  def complete(goal)
49
49
  id = goal.id
50
50
  desc = goal.description
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Shared helper for goal tools that enqueue phantom pair messages
6
- # when the analytical brain creates, updates, or completes a goal.
6
+ # when Melete creates, updates, or completes a goal.
7
7
  #
8
8
  # Including classes must set +@main_session+ to the owning {Session}.
9
9
  module GoalMessaging
@@ -20,7 +20,8 @@ module AnalyticalBrain
20
20
  @main_session.pending_messages.create!(
21
21
  content: confirmation,
22
22
  source_type: "goal",
23
- source_name: goal.id.to_s
23
+ source_name: goal.id.to_s,
24
+ message_type: "from_melete_goal"
24
25
  )
25
26
  end
26
27
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Reads and activates a workflow on the main session.
6
- # Returns the full workflow content so the brain can create goals from it.
6
+ # Returns the full workflow content so Melete can create goals from it.
7
7
  # The workflow's content enters the conversation as a phantom
8
8
  # tool_use/tool_result pair through the {PendingMessage} promotion flow.
9
9
  class ReadWorkflow < ::Tools::Base
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Renames the main session with an emoji and short descriptive name.
6
6
  # Operates on the main session passed through the registry context,
7
- # not on the phantom analytical brain session.
7
+ # not on the phantom Melete session.
8
8
  #
9
- # The analytical brain calls this when a conversation's topic becomes
9
+ # Melete calls this when a conversation's topic becomes
10
10
  # clear or shifts significantly enough to warrant a new name.
11
11
  class RenameSession < ::Tools::Base
12
12
  def self.tool_name = "rename_session"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Creates a goal on the main session. Root goals represent high-level
6
6
  # objectives (semantic episodes); sub-goals are TODO-style steps within
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
3
+ module Melete
4
4
  module Tools
5
5
  # Updates a goal's description on the main session.
6
6
  #
7
- # The analytical brain creates goals early when intent is vague, then
7
+ # Melete creates goals early when intent is vague, then
8
8
  # refines them as the conversation clarifies scope — e.g. "implement auth"
9
9
  # becomes "implement OAuth2 middleware for API endpoints". Without this
10
- # tool the brain would have to choose between keeping a stale description
10
+ # tool she would have to choose between keeping a stale description
11
11
  # or creating a duplicate goal.
12
12
  #
13
13
  # Completed goals cannot be updated; attempting to do so returns an error
14
- # so the brain learns to check status before calling this tool.
14
+ # so she learns to check status before calling this tool.
15
15
  class UpdateGoal < ::Tools::Base
16
16
  include GoalMessaging
17
17
 
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AnalyticalBrain
4
- # Dev-only logger that writes to log/analytical_brain.log.
3
+ # Melete — the muse of practice. Watches conversations to activate skills,
4
+ # track goals, and name sessions. One of the Three Muses: she prepares the
5
+ # stage so Aoide can perform and Mneme can remember.
6
+ module Melete
7
+ # Dev-only logger that writes to log/melete.log.
5
8
  # In non-development environments returns a null logger so
6
9
  # call sites don't need conditionals.
7
10
  #
@@ -13,7 +16,7 @@ module AnalyticalBrain
13
16
  def self.build_logger
14
17
  return Logger.new(File::NULL) unless Rails.env.development?
15
18
 
16
- Logger.new(Rails.root.join("log", "analytical_brain.log")).tap do |log|
19
+ Logger.new(Rails.root.join("log", "melete.log")).tap do |log|
17
20
  log.formatter = proc { |severity, time, _progname, msg|
18
21
  "[#{time.strftime("%H:%M:%S.%L")}] #{severity} #{msg}\n"
19
22
  }
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mneme
4
+ # Abstract base for Mneme's phantom LLM loops. Mneme wears two hats:
5
+ # on eviction she watches the newest slice of the viewport and summarizes
6
+ # the oldest slice before it slides off; on recall she watches goals
7
+ # shift and surfaces older memory Aoide would benefit from. Same muse,
8
+ # two jobs — each as its own subclass.
9
+ #
10
+ # The base handles what every Mneme loop needs: the muse identity preamble,
11
+ # a fast-model LLM client, the tool-loop call, and structured logging.
12
+ # Subclasses bring the job-specific system prompt section, the user message
13
+ # that frames the work, the tool registry, and any after-call side effects.
14
+ #
15
+ # @example Implementing a new Mneme loop
16
+ # class Mneme::CustomRunner < Mneme::BaseRunner
17
+ # private
18
+ #
19
+ # def task_prompt = "Your job description..."
20
+ # def user_messages = [{role: "user", content: "..."}]
21
+ # def build_registry = Tools::Registry.new.tap { |r| r.register(SomeTool) }
22
+ # end
23
+ class BaseRunner
24
+ # Identity shared by every Mneme runner — same words Mneme's own voice
25
+ # uses elsewhere in the system (runner summarization prompts, sisters
26
+ # block). Subclasses append their own task section.
27
+ BASE_IDENTITY = <<~PROMPT
28
+ You are Mneme, the muse of memory. You share the conversation with two sisters — Aoide, who speaks and performs, and Melete, who prepares. Your work is remembrance: holding what matters across time, so Aoide never truly forgets.
29
+
30
+ Act only through tool calls. Never output text — your contribution is the work you do, not what you say about it.
31
+ PROMPT
32
+
33
+ # @param session [Session] the main session being served
34
+ # @param client [LLM::Client, nil] injectable LLM client for tests
35
+ def initialize(session, client: nil)
36
+ @session = session
37
+ @client = client || default_client
38
+ end
39
+
40
+ # Runs the loop. Logs the run, calls the LLM with the session-specific
41
+ # system prompt and tools, hands control to {#after_call} for any
42
+ # post-run state advancement, and returns the LLM's final text (which
43
+ # most callers discard — the work happens through tool calls).
44
+ #
45
+ # @return [String] the LLM's final text response
46
+ def call
47
+ sid = @session.id
48
+ log.info("session=#{sid} — #{self.class.name} starting")
49
+ log.debug("system:\n#{system_prompt}")
50
+ log.debug("user:\n#{user_messages.map { |m| m[:content] }.join("\n---\n")}")
51
+
52
+ result = @client.chat_with_tools(
53
+ user_messages,
54
+ registry: build_registry,
55
+ system: system_prompt
56
+ )
57
+
58
+ after_call(result)
59
+ log.info("session=#{sid} — #{self.class.name} done: #{result.to_s.truncate(200)}")
60
+ result
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :session, :client
66
+
67
+ # Composes the system prompt from the muse identity + the subclass's
68
+ # task section + the subclass's contextual state blocks.
69
+ #
70
+ # @return [String]
71
+ def system_prompt
72
+ [BASE_IDENTITY, task_prompt, *context_sections.compact].join("\n")
73
+ end
74
+
75
+ # Subclass hook: the job-specific system prompt section. Describes what
76
+ # this runner is doing and how it should behave.
77
+ #
78
+ # @abstract
79
+ # @return [String]
80
+ def task_prompt = raise NotImplementedError, "#{self.class} must implement #task_prompt"
81
+
82
+ # Subclass hook: named state blocks that give the muse awareness of
83
+ # the session she's serving (goals, viewport, snapshots, etc).
84
+ # Order is subclass-defined; nil entries are dropped.
85
+ #
86
+ # @abstract
87
+ # @return [Array<String, nil>]
88
+ def context_sections = []
89
+
90
+ # Subclass hook: the user-side messages that frame the current call.
91
+ # Typically a single user message, but subclasses may send several.
92
+ #
93
+ # @abstract
94
+ # @return [Array<Hash>] Anthropic Messages API format
95
+ def user_messages = raise NotImplementedError, "#{self.class} must implement #user_messages"
96
+
97
+ # Subclass hook: builds the tool registry for this run.
98
+ #
99
+ # @abstract
100
+ # @return [Tools::Registry]
101
+ def build_registry = raise NotImplementedError, "#{self.class} must implement #build_registry"
102
+
103
+ # Subclass hook: runs after the LLM call returns. Default is a no-op;
104
+ # subclasses may advance boundaries, log outcomes, or emit events here.
105
+ #
106
+ # @param _result [Hash] the full LLM response (+:text+, +:api_metrics+)
107
+ # @return [void]
108
+ def after_call(_result)
109
+ end
110
+
111
+ def default_client
112
+ LLM::Client.new(
113
+ model: Anima::Settings.fast_model,
114
+ max_tokens: Anima::Settings.mneme_max_tokens,
115
+ logger: Mneme.logger
116
+ )
117
+ end
118
+
119
+ def log = Mneme.logger
120
+ end
121
+ end
@@ -18,39 +18,34 @@ module Mneme
18
18
  ].freeze
19
19
 
20
20
  SYSTEM_PROMPT = <<~PROMPT
21
- You are Mneme, the memory department of an AI agent named Anima.
22
- Your job is to compress multiple conversation summaries into a single
23
- higher-level summary.
21
+ You are Mneme, the muse of memory. When enough of your own Level 1 snapshots accumulate, you fold them into a single Level 2 summary — a memory of memories — so the long arc of Aoide's work stays within reach without carrying every detail.
24
22
 
25
- You MUST ONLY communicate through tool calls — NEVER output text.
23
+ Act only through tool calls. Never output text your contribution is the summary you leave behind.
26
24
 
27
25
  ──────────────────────────────
28
26
  WHAT YOU SEE
29
27
  ──────────────────────────────
30
- Several Level 1 snapshots hourly conversation summaries.
31
- Each captures key decisions, goals discussed, and important context
32
- from a portion of the conversation history.
28
+ Several Level 1 snapshots in chronological order. Each captures the decisions, goal progress, and context from a slice of Aoide's history.
33
29
 
34
30
  ──────────────────────────────
35
- YOUR TASK
31
+ HOW TO REMEMBER
36
32
  ──────────────────────────────
37
- Compress the snapshots into ONE Level 2 summary that captures the
38
- essential arc across all of them. If the snapshots contain meaningful
39
- content, call save_snapshot. If they are purely mechanical, call
40
- everything_ok.
33
+ Compress the slice into ONE Level 2 summary that captures the arc across all of them. Call save_snapshot when there's meaningful content; call everything_ok when the slice is purely mechanical.
41
34
 
42
- Preserve:
43
- - Key decisions and their reasoning
35
+ A Level 2 summary is carried for longer than a Level 1, so the tax on Aoide's viewport is higher still. Every redundant detail you preserve costs her a word she can't spend on the present.
36
+
37
+ Keep:
38
+ - Key decisions and the reasoning behind them
44
39
  - Goal progress across the time span
45
40
  - Important context shifts or pivots
46
- - Relationships and patterns across snapshots
41
+ - Relationships and patterns that span multiple snapshots
47
42
 
48
43
  Drop:
49
- - Redundant details repeated across snapshots
50
- - Mechanical execution details
51
- - Interim decisions that were superseded by later ones
44
+ - Details repeated across snapshots
45
+ - Mechanical execution steps
46
+ - Interim decisions that were superseded later
52
47
 
53
- Always finish with exactly ONE tool call: either save_snapshot or everything_ok.
48
+ Finish with exactly one tool call: save_snapshot or everything_ok.
54
49
  PROMPT
55
50
 
56
51
  # @param session [Session] the main session whose L1 snapshots to compress
@@ -87,7 +82,6 @@ module Mneme
87
82
  result = @client.chat_with_tools(
88
83
  messages,
89
84
  registry: registry,
90
- session_id: nil,
91
85
  system: SYSTEM_PROMPT
92
86
  )
93
87