anima-core 1.3.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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +23 -26
  3. data/README.md +118 -104
  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 +16 -5
  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 +23 -11
  30. data/app/models/message.rb +46 -48
  31. data/app/models/pending_message.rb +407 -12
  32. data/app/models/pinned_message.rb +8 -3
  33. data/app/models/session.rb +660 -566
  34. data/app/models/snapshot.rb +11 -21
  35. data/bin/inspect-cassette +157 -0
  36. data/bin/release +212 -0
  37. data/bin/with-llms +20 -0
  38. data/config/application.rb +1 -0
  39. data/config/database.yml +1 -0
  40. data/config/initializers/event_subscribers.rb +71 -4
  41. data/config/initializers/inflections.rb +3 -1
  42. data/db/cable_structure.sql +9 -0
  43. data/db/migrate/20260330120000_add_source_to_pending_messages.rb +8 -0
  44. data/db/migrate/20260401180000_add_api_metrics_to_messages.rb +7 -0
  45. data/db/migrate/20260401210935_remove_recalled_message_ids_from_sessions.rb +5 -0
  46. data/db/migrate/20260403080031_add_initial_cwd_to_sessions.rb +5 -0
  47. data/db/migrate/20260407170803_remove_viewport_message_ids_from_sessions.rb +5 -0
  48. data/db/migrate/20260407180400_remove_mneme_snapshot_pointer_columns_from_sessions.rb +6 -0
  49. data/db/migrate/20260411120553_add_token_count_to_pinned_messages.rb +5 -0
  50. data/db/migrate/20260411172926_remove_active_skills_and_workflow_from_sessions.rb +6 -0
  51. data/db/migrate/20260412110625_replace_processing_with_aasm_state.rb +6 -0
  52. data/db/migrate/20260418150323_add_kind_and_message_type_to_pending_messages.rb +6 -0
  53. data/db/migrate/20260419120000_add_drain_fields_to_pending_messages.rb +7 -0
  54. data/db/migrate/20260419130000_drop_pending_messages_kind_default.rb +5 -0
  55. data/db/migrate/20260419140000_add_drain_indexes_to_pending_messages.rb +8 -0
  56. data/db/migrate/20260420100000_add_hud_visibility_to_sessions.rb +15 -0
  57. data/db/queue_structure.sql +61 -0
  58. data/db/structure.sql +133 -0
  59. data/lib/agents/registry.rb +1 -1
  60. data/lib/anima/cli.rb +41 -13
  61. data/lib/anima/installer.rb +13 -0
  62. data/lib/anima/settings.rb +16 -36
  63. data/lib/anima/version.rb +1 -1
  64. data/lib/events/authentication_required.rb +24 -0
  65. data/lib/events/bounce_back.rb +4 -4
  66. data/lib/events/eviction_completed.rb +28 -0
  67. data/lib/events/goal_created.rb +28 -0
  68. data/lib/events/goal_updated.rb +32 -0
  69. data/lib/events/llm_responded.rb +35 -0
  70. data/lib/events/message_created.rb +27 -0
  71. data/lib/events/message_updated.rb +25 -0
  72. data/lib/events/session_state_changed.rb +30 -0
  73. data/lib/events/skill_activated.rb +28 -0
  74. data/lib/events/start_melete.rb +36 -0
  75. data/lib/events/start_mneme.rb +33 -0
  76. data/lib/events/start_processing.rb +32 -0
  77. data/lib/events/subagent_evicted.rb +31 -0
  78. data/lib/events/subscribers/active_state_broadcaster.rb +27 -0
  79. data/lib/events/subscribers/authentication_broadcaster.rb +34 -0
  80. data/lib/events/subscribers/drain_kickoff.rb +20 -0
  81. data/lib/events/subscribers/eviction_broadcaster.rb +26 -0
  82. data/lib/events/subscribers/llm_response_handler.rb +111 -0
  83. data/lib/events/subscribers/melete_kickoff.rb +24 -0
  84. data/lib/events/subscribers/message_broadcaster.rb +34 -0
  85. data/lib/events/subscribers/mneme_kickoff.rb +24 -0
  86. data/lib/events/subscribers/mneme_scheduler.rb +21 -0
  87. data/lib/events/subscribers/persister.rb +8 -9
  88. data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
  89. data/lib/events/subscribers/subagent_message_router.rb +28 -34
  90. data/lib/events/subscribers/subagent_visibility_broadcaster.rb +33 -0
  91. data/lib/events/subscribers/tool_response_creator.rb +33 -0
  92. data/lib/events/subscribers/transient_broadcaster.rb +1 -1
  93. data/lib/events/tool_executed.rb +34 -0
  94. data/lib/events/workflow_activated.rb +27 -0
  95. data/lib/llm/client.rb +46 -199
  96. data/lib/mcp/client_manager.rb +41 -46
  97. data/lib/mcp/stdio_transport.rb +9 -5
  98. data/lib/{analytical_brain → melete}/runner.rb +73 -68
  99. data/lib/{analytical_brain → melete}/tools/activate_skill.rb +3 -3
  100. data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +3 -3
  101. data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
  102. data/lib/{analytical_brain → melete}/tools/finish_goal.rb +6 -3
  103. data/lib/melete/tools/goal_messaging.rb +29 -0
  104. data/lib/{analytical_brain → melete}/tools/read_workflow.rb +4 -4
  105. data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
  106. data/lib/{analytical_brain → melete}/tools/set_goal.rb +6 -2
  107. data/lib/{analytical_brain → melete}/tools/update_goal.rb +9 -5
  108. data/lib/{analytical_brain.rb → melete.rb} +6 -3
  109. data/lib/mneme/base_runner.rb +121 -0
  110. data/lib/mneme/l2_runner.rb +14 -20
  111. data/lib/mneme/recall_runner.rb +132 -0
  112. data/lib/mneme/runner.rb +123 -165
  113. data/lib/mneme/search.rb +104 -62
  114. data/lib/mneme/tools/nothing_to_surface.rb +25 -0
  115. data/lib/mneme/tools/save_snapshot.rb +2 -10
  116. data/lib/mneme/tools/surface_memory.rb +89 -0
  117. data/lib/mneme.rb +11 -5
  118. data/lib/providers/anthropic.rb +112 -7
  119. data/lib/shell_session.rb +290 -432
  120. data/lib/skills/definition.rb +2 -2
  121. data/lib/skills/registry.rb +1 -1
  122. data/lib/tools/base.rb +16 -1
  123. data/lib/tools/bash.rb +25 -55
  124. data/lib/tools/edit.rb +2 -0
  125. data/lib/tools/mark_goal_completed.rb +4 -5
  126. data/lib/tools/read.rb +2 -0
  127. data/lib/tools/registry.rb +85 -4
  128. data/lib/tools/response_truncator.rb +1 -1
  129. data/lib/tools/{recall.rb → search_messages.rb} +19 -21
  130. data/lib/tools/spawn_specialist.rb +22 -14
  131. data/lib/tools/spawn_subagent.rb +30 -20
  132. data/lib/tools/subagent_prompts.rb +17 -19
  133. data/lib/tools/think.rb +1 -1
  134. data/lib/tools/{remember.rb → view_messages.rb} +10 -10
  135. data/lib/tools/write.rb +2 -0
  136. data/lib/tui/app.rb +393 -149
  137. data/lib/tui/braille_spinner.rb +7 -7
  138. data/lib/tui/cable_client.rb +9 -16
  139. data/lib/tui/decorators/base_decorator.rb +47 -6
  140. data/lib/tui/decorators/bash_decorator.rb +1 -1
  141. data/lib/tui/decorators/edit_decorator.rb +4 -2
  142. data/lib/tui/decorators/read_decorator.rb +4 -2
  143. data/lib/tui/decorators/think_decorator.rb +2 -2
  144. data/lib/tui/decorators/web_get_decorator.rb +1 -1
  145. data/lib/tui/decorators/write_decorator.rb +4 -2
  146. data/lib/tui/flash.rb +19 -14
  147. data/lib/tui/formatting.rb +20 -9
  148. data/lib/tui/input_buffer.rb +6 -6
  149. data/lib/tui/message_store.rb +165 -28
  150. data/lib/tui/performance_logger.rb +2 -3
  151. data/lib/tui/screens/chat.rb +149 -79
  152. data/lib/tui/settings.rb +93 -0
  153. data/lib/workflows/definition.rb +3 -3
  154. data/lib/workflows/registry.rb +1 -1
  155. data/skills/github.md +38 -0
  156. data/templates/config.toml +16 -32
  157. data/templates/tui.toml +209 -0
  158. data/workflows/review_pr.md +18 -14
  159. metadata +98 -29
  160. data/app/jobs/agent_request_job.rb +0 -199
  161. data/app/jobs/analytical_brain_job.rb +0 -33
  162. data/app/jobs/count_message_tokens_job.rb +0 -39
  163. data/app/jobs/passive_recall_job.rb +0 -29
  164. data/app/models/concerns/message/broadcasting.rb +0 -85
  165. data/config/initializers/fts5_schema_dump.rb +0 -21
  166. data/lib/agent_loop.rb +0 -186
  167. data/lib/analytical_brain/tools/deactivate_skill.rb +0 -39
  168. data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -34
  169. data/lib/environment_probe.rb +0 -232
  170. data/lib/events/agent_message.rb +0 -11
  171. data/lib/events/subscribers/message_collector.rb +0 -64
  172. data/lib/events/tool_call.rb +0 -31
  173. data/lib/events/tool_response.rb +0 -33
  174. data/lib/mneme/compressed_viewport.rb +0 -200
  175. data/lib/mneme/passive_recall.rb +0 -69
@@ -3,81 +3,76 @@
3
3
  require "mcp"
4
4
 
5
5
  module Mcp
6
- # Manages MCP client connections and registers their tools with
7
- # {Tools::Registry}. Each configured server (HTTP or stdio) gets
8
- # a dedicated {MCP::Client} instance. Tool lists are fetched once
9
- # during registration and cached in the registrysubsequent LLM
10
- # turns reuse the same tool set without re-querying servers.
6
+ # Connects to MCP servers and registers their tools with
7
+ # {Tools::Registry}. Each configured server (HTTP or stdio) gets a
8
+ # dedicated {MCP::Client} instance, cached for the worker's
9
+ # lifetime. Connection failures are logged and skippeda
10
+ # misconfigured or unavailable server does not prevent other servers
11
+ # or built-in tools from working.
11
12
  #
12
- # Connection failures are logged and skipped a misconfigured or
13
- # unavailable server does not prevent other servers or built-in
14
- # tools from working.
13
+ # Spawned stdio processes are reaped on worker exit via
14
+ # {Mcp::StdioTransport.cleanup_all}.
15
+ #
16
+ # The cache is built once on the first {#register_tools} call and
17
+ # never invalidated; edits to +mcp.toml+ require a worker restart.
15
18
  #
16
19
  # @example
17
- # manager = Mcp::ClientManager.new
18
- # manager.register_tools(registry)
20
+ # Mcp::ClientManager.shared.register_tools(registry)
19
21
  class ClientManager
22
+ # Lazily-instantiated process-wide manager. Production code should
23
+ # call {.shared}; {.new} is reserved for tests and internal use.
24
+ # @return [Mcp::ClientManager]
25
+ def self.shared
26
+ @shared ||= new
27
+ end
28
+
20
29
  # @param config [Mcp::Config] injectable config for testing
21
30
  def initialize(config: Config.new(logger: Rails.logger))
22
31
  @config = config
23
32
  end
24
33
 
25
- # Connects to all configured MCP servers and registers their tools
26
- # in the given registry. Returns warnings for servers that failed
27
- # to load so the caller can surface them to the user.
34
+ # Connects to every configured MCP server on first call, caches
35
+ # the resulting tool wrappers, and registers them in the given
36
+ # registry.
28
37
  #
29
38
  # @param registry [Tools::Registry] the registry to add tools to
30
- # @return [Array<String>] warning messages for servers that failed
39
+ # @return [Array<String>] warning messages from configuration plus
40
+ # any per-server load failures
31
41
  def register_tools(registry)
32
- warnings = []
33
- register_transport_tools(@config.http_servers, registry, warnings) { |server| build_http_client(server) }
34
- register_transport_tools(@config.stdio_servers, registry, warnings) { |server| build_stdio_client(server) }
35
- @config.warnings + warnings
42
+ load_servers if @wrappers.nil?
43
+ @wrappers.each { |wrapper| registry.register(wrapper) }
44
+ @config.warnings + @warnings
36
45
  end
37
46
 
38
47
  private
39
48
 
40
- # Iterates server configs, builds a client for each via the block,
41
- # and registers the server's tools. Failures are logged and collected.
42
- #
43
- # @param servers [Array<Hash>] server configs from {Mcp::Config}
44
- # @param registry [Tools::Registry] registry to register tools in
45
- # @param warnings [Array<String>] collects failure messages
46
- # @yield [server] block that builds an {MCP::Client} for the server
47
- def register_transport_tools(servers, registry, warnings)
49
+ def load_servers
50
+ @wrappers = []
51
+ @warnings = []
52
+ register_transport_tools(@config.http_servers) { |server| build_http_client(server) }
53
+ register_transport_tools(@config.stdio_servers) { |server| build_stdio_client(server) }
54
+ end
55
+
56
+ def register_transport_tools(servers)
48
57
  servers.each do |server|
49
58
  client = yield(server)
50
- register_server_tools(server[:name], client, registry)
59
+ wrappers = client.tools.map { |mcp_tool|
60
+ Tools::McpTool.new(server_name: server[:name], mcp_client: client, mcp_tool: mcp_tool)
61
+ }
62
+ @wrappers.concat(wrappers)
63
+ Rails.logger.info("MCP: registered #{wrappers.size} tools from #{server[:name]}")
51
64
  rescue => error
52
65
  message = "MCP: failed to load tools from #{server[:name]}: #{error.message}"
53
66
  Rails.logger.warn(message)
54
- warnings << message
67
+ @warnings << message
55
68
  end
56
69
  end
57
70
 
58
- # Fetches tools from an MCP client and registers them with
59
- # namespaced names in the registry.
60
- #
61
- # @param server_name [String] server name for tool namespacing
62
- # @param client [MCP::Client] connected MCP client
63
- # @param registry [Tools::Registry] registry to register tools in
64
- def register_server_tools(server_name, client, registry)
65
- count = client.tools.map { |mcp_tool|
66
- Tools::McpTool.new(server_name: server_name, mcp_client: client, mcp_tool: mcp_tool)
67
- }.each { |wrapper| registry.register(wrapper) }.size
68
-
69
- Rails.logger.info("MCP: registered #{count} tools from #{server_name}")
70
- end
71
-
72
- # @param server [Hash] server config with +:url+ and +:headers+
73
- # @return [MCP::Client]
74
71
  def build_http_client(server)
75
72
  transport = MCP::Client::HTTP.new(url: server[:url], headers: server[:headers])
76
73
  MCP::Client.new(transport: transport)
77
74
  end
78
75
 
79
- # @param server [Hash] server config with +:command+, +:args+, +:env+
80
- # @return [MCP::Client]
81
76
  def build_stdio_client(server)
82
77
  transport = StdioTransport.new(command: server[:command], args: server[:args], env: server[:env])
83
78
  MCP::Client.new(transport: transport)
@@ -115,8 +115,11 @@ module Mcp
115
115
  @wait_thread&.alive? || false
116
116
  end
117
117
 
118
+ # +pgroup: true+ so {#terminate_process} can group-signal the
119
+ # entire descendant tree — npm/npx wrappers leak their +node+
120
+ # children otherwise.
118
121
  def spawn_process
119
- @stdin, @stdout, @wait_thread = Open3.popen2(@env, @command, *@args)
122
+ @stdin, @stdout, @wait_thread = Open3.popen2(@env, @command, *@args, pgroup: true)
120
123
  @stdin.set_encoding("UTF-8")
121
124
  @stdout.set_encoding("UTF-8")
122
125
  self.class.register(self)
@@ -164,14 +167,15 @@ module Mcp
164
167
  @stdout&.close rescue IOError # rubocop:disable Style/RescueModifier
165
168
  end
166
169
 
167
- # Sends SIGTERM and waits up to 2 seconds for the process to exit.
168
- # Falls back to SIGKILL if the process does not terminate in time.
170
+ # Sends SIGTERM to the process group; escalates to SIGKILL on the
171
+ # group after +GRACEFUL_SHUTDOWN_TIMEOUT+ seconds. Negative PID
172
+ # signals the whole group (see {#spawn_process}).
169
173
  def terminate_process
170
174
  return unless @wait_thread
171
175
 
172
176
  pid = @wait_thread.pid
173
177
  begin
174
- Process.kill("TERM", pid)
178
+ Process.kill("TERM", -pid)
175
179
  rescue Errno::ESRCH, Errno::EPERM
176
180
  return
177
181
  end
@@ -181,7 +185,7 @@ module Mcp
181
185
  _, status = Process.wait2(pid, Process::WNOHANG)
182
186
  break if status
183
187
  if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
184
- Process.kill("KILL", pid) rescue Errno::ESRCH # rubocop:disable Style/RescueModifier
188
+ Process.kill("KILL", -pid) rescue Errno::ESRCH # rubocop:disable Style/RescueModifier
185
189
  Process.wait(pid) rescue Errno::ECHILD # rubocop:disable Style/RescueModifier
186
190
  break
187
191
  end
@@ -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
@@ -272,9 +267,15 @@ module AnalyticalBrain
272
267
  SECTION
273
268
  end
274
269
 
275
- # @return [String] available skills list for the analytical brain
270
+ # Skills already visible in the viewport are excluded from the catalog
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.
273
+ #
274
+ # @see Session#skills_in_viewport
275
+ # @return [String] available skills list for Melete
276
276
  def skills_catalog_section
277
- catalog = Skills::Registry.instance.catalog
277
+ present = @session.skills_in_viewport
278
+ catalog = Skills::Registry.instance.catalog.except(*present)
278
279
  items = if catalog.empty?
279
280
  "None"
280
281
  else
@@ -288,9 +289,13 @@ module AnalyticalBrain
288
289
  SECTION
289
290
  end
290
291
 
291
- # @return [String] available workflows list for the analytical brain
292
+ # Workflows already visible in the viewport are excluded from the catalog.
293
+ #
294
+ # @see Session#workflow_in_viewport
295
+ # @return [String] available workflows list for Melete
292
296
  def workflows_catalog_section
293
- catalog = Workflows::Registry.instance.catalog
297
+ present = @session.workflow_in_viewport
298
+ catalog = Workflows::Registry.instance.catalog.reject { |name, _| name == present }
294
299
  items = if catalog.empty?
295
300
  "None"
296
301
  else
@@ -304,13 +309,13 @@ module AnalyticalBrain
304
309
  SECTION
305
310
  end
306
311
 
307
- # @return [String, nil] active goals for the brain's own context,
308
- # 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
309
314
  def active_goals_section
310
315
  root_goals = @session.goals.root.includes(:sub_goals).active.order(:created_at)
311
316
  return if root_goals.empty?
312
317
 
313
- lines = root_goals.map { |goal| format_goal_for_brain(goal) }
318
+ lines = root_goals.map { |goal| format_goal_for_melete(goal) }
314
319
  <<~SECTION
315
320
  ──────────────────────────────
316
321
  ACTIVE GOALS
@@ -320,14 +325,14 @@ module AnalyticalBrain
320
325
  end
321
326
 
322
327
  # Formats a root goal and its sub-goals as a markdown checklist
323
- # with IDs so the brain can reference them in finish_goal calls.
328
+ # with IDs so Melete can reference them in finish_goal calls.
324
329
  #
325
330
  # @example
326
331
  # "- Implement feature X (id: 42)\n - [x] Read code (id: 43)\n - [ ] Write tests (id: 44)"
327
332
  #
328
333
  # @param goal [Goal] root goal with preloaded sub_goals
329
- # @return [String] goal formatted as markdown checklist for brain context
330
- 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)
331
336
  parts = ["- #{goal.description} (id: #{goal.id})"]
332
337
  goal.sub_goals.sort_by(&:created_at).each do |sub|
333
338
  checkbox = (sub.status == "completed") ? "[x]" : "[ ]"
@@ -336,8 +341,8 @@ module AnalyticalBrain
336
341
  parts.join("\n")
337
342
  end
338
343
 
339
- # @return [Logger] dev-only analytical brain logger
340
- def log = AnalyticalBrain.logger
344
+ # @return [Logger] dev-only Melete logger
345
+ def log = Melete.logger
341
346
 
342
347
  # @return [Tools::Registry] registry with tools from active responsibilities
343
348
  def build_registry
@@ -1,10 +1,10 @@
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
- # The skill's content is injected into the main agent's system prompt,
7
- # making the knowledge available for the current and future responses.
6
+ # The skill's content enters the conversation as a phantom
7
+ # tool_use/tool_result pair through the {PendingMessage} promotion flow.
8
8
  class ActivateSkill < ::Tools::Base
9
9
  def self.tool_name = "activate_skill"
10
10
 
@@ -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/
@@ -47,7 +47,7 @@ module AnalyticalBrain
47
47
  return error if error
48
48
 
49
49
  @session.update!(name: nickname)
50
- "Nickname set to @#{nickname}"
50
+ "Nickname set to #{nickname}"
51
51
  end
52
52
 
53
53
  private
@@ -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,10 +1,12 @@
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.
7
7
  class FinishGoal < ::Tools::Base
8
+ include GoalMessaging
9
+
8
10
  def self.tool_name = "finish_goal"
9
11
 
10
12
  def self.description = "Mark a goal as completed."
@@ -41,8 +43,8 @@ module AnalyticalBrain
41
43
  # active sub-goals within a single transaction so the after_commit
42
44
  # broadcast includes the fully cascaded state.
43
45
  #
44
- # Returns an error for already-completed goals so the analytical
45
- # brain learns to check status before retrying.
46
+ # Returns an error for already-completed goals so Melete
47
+ # learns to check status before retrying.
46
48
  def complete(goal)
47
49
  id = goal.id
48
50
  desc = goal.description
@@ -57,6 +59,7 @@ module AnalyticalBrain
57
59
 
58
60
  msg = "Goal completed: #{desc} (id: #{id})"
59
61
  msg += " (released #{released} orphaned pins)" if released > 0
62
+ enqueue_goal_message(goal, msg)
60
63
  msg
61
64
  end
62
65
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Melete
4
+ module Tools
5
+ # Shared helper for goal tools that enqueue phantom pair messages
6
+ # when Melete creates, updates, or completes a goal.
7
+ #
8
+ # Including classes must set +@main_session+ to the owning {Session}.
9
+ module GoalMessaging
10
+ private
11
+
12
+ # Enqueues a goal event as a {PendingMessage} on the main session.
13
+ # Promoted to a phantom tool_use/tool_result pair so the main agent
14
+ # sees "I recalled this goal event" in its conversation history.
15
+ #
16
+ # @param goal [Goal] the goal that changed
17
+ # @param confirmation [String] human-readable event description
18
+ # @return [PendingMessage]
19
+ def enqueue_goal_message(goal, confirmation)
20
+ @main_session.pending_messages.create!(
21
+ content: confirmation,
22
+ source_type: "goal",
23
+ source_name: goal.id.to_s,
24
+ message_type: "from_melete_goal"
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,11 +1,11 @@
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.
7
- # Also sets the workflow as active on the session, injecting its content
8
- # into the main agent's "Your Expertise" section.
6
+ # Returns the full workflow content so Melete can create goals from it.
7
+ # The workflow's content enters the conversation as a phantom
8
+ # tool_use/tool_result pair through the {PendingMessage} promotion flow.
9
9
  class ReadWorkflow < ::Tools::Base
10
10
  def self.tool_name = "read_workflow"
11
11
 
@@ -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,11 +1,13 @@
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
7
7
  # a root goal. The two-level hierarchy is enforced by the Goal model.
8
8
  class SetGoal < ::Tools::Base
9
+ include GoalMessaging
10
+
9
11
  def self.tool_name = "set_goal"
10
12
 
11
13
  def self.description = "Create a goal or sub-goal."
@@ -40,7 +42,9 @@ module AnalyticalBrain
40
42
  description: description,
41
43
  parent_goal_id: input["parent_goal_id"]
42
44
  )
43
- format_confirmation(goal)
45
+ confirmation = format_confirmation(goal)
46
+ enqueue_goal_message(goal, confirmation)
47
+ confirmation
44
48
  rescue ActiveRecord::RecordInvalid => error
45
49
  {error: error.record.errors.full_messages.join(", ")}
46
50
  end