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,204 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mneme
4
- # Builds a compressed viewport for Mneme's LLM context. Mneme sees
5
- # conversation (user/agent messages and think events) but not mechanical
6
- # execution (tool calls and responses). Tool calls are compressed to
7
- # aggregate counters like `[4 tools called]`.
8
- #
9
- # The viewport is split into three zones separated by delimiters:
10
- # - **Eviction zone** — messages about to leave the viewport (upper third)
11
- # - **Middle zone** — messages in the middle of the viewport
12
- # - **Recent zone** — the most recent messages (lower third)
13
- #
14
- # Zone boundaries are calculated WITH tool call tokens (they affect
15
- # position), then tool calls are removed and replaced with counters.
16
- #
17
- # @example
18
- # viewport = Mneme::CompressedViewport.new(session, token_budget: 60_000)
19
- # viewport.render #=> "── EVICTION ZONE ──\nmessage 42 User: ..."
20
- class CompressedViewport
21
- ZONE_DELIMITERS = {
22
- eviction: "── EVICTION ZONE (upper third) ──",
23
- middle: "── MIDDLE ZONE ──",
24
- recent: "── RECENT ZONE (lower third) ──"
25
- }.freeze
26
-
27
- # @param session [Session] the session to build viewport for
28
- # @param token_budget [Integer] total tokens available for Mneme's viewport
29
- # @param from_message_id [Integer, nil] start from this message ID (inclusive);
30
- # when nil, uses the session's full viewport
31
- def initialize(session, token_budget:, from_message_id: nil)
32
- @session = session
33
- @token_budget = token_budget
34
- @from_message_id = from_message_id
35
- end
36
-
37
- # Renders the compressed viewport as a string ready for Mneme's LLM context.
38
- #
39
- # @return [String] compressed viewport with zone delimiters
40
- def render
41
- return "" if messages.empty?
42
-
43
- zones = split_into_zones(messages)
44
- render_zones(zones)
45
- end
46
-
47
- # @return [Array<Message>] the raw messages selected for this viewport
48
- def messages
49
- @messages ||= fetch_messages
50
- end
51
-
52
- private
53
-
54
- # Fetches messages within token budget, starting from from_message_id.
55
- # Walks oldest-first from the boundary so Mneme processes the eviction
56
- # zone (oldest messages) rather than the recent zone. This ensures
57
- # {Mneme::Runner#advance_boundary} advances past only the oldest third,
58
- # preserving recent conversation context in the main viewport.
59
- #
60
- # Caches per-message token costs in @message_costs for reuse by split_into_zones.
61
- #
62
- # @return [Array<Message>] chronologically ordered (oldest first)
63
- def fetch_messages
64
- scope = @session.messages.context_messages
65
-
66
- if @from_message_id
67
- scope = scope.where("id >= ?", @from_message_id)
68
- end
69
-
70
- selected = []
71
- @message_costs = {}
72
- remaining = @token_budget
73
-
74
- scope.reorder(id: :asc).each do |message|
75
- cost = message_token_cost(message)
76
- break if cost > remaining && selected.any?
77
-
78
- selected << message
79
- @message_costs[message.id] = cost
80
- remaining -= cost
81
- end
82
-
83
- selected
84
- end
85
-
86
- # Splits messages into three zones by token count.
87
- # Zone boundaries are calculated including ALL messages (tool calls count
88
- # toward position), but zone assignment uses cumulative tokens.
89
- #
90
- # @return [Hash{Symbol => Array<Message>}] :eviction, :middle, :recent
91
- def split_into_zones(messages)
92
- costs = messages.map { |message| [message, @message_costs[message.id] || message_token_cost(message)] }
93
- zone_size = costs.sum(&:last) / 3.0
94
-
95
- result = {eviction: [], middle: [], recent: []}
96
- cumulative = 0
97
-
98
- costs.each do |message, cost|
99
- cumulative += cost
100
- result[zone_for_cumulative(cumulative, zone_size)] << message
101
- end
102
-
103
- result
104
- end
105
-
106
- # Renders zones with delimiters, compressing tool calls into counters.
107
- #
108
- # @param zones [Hash{Symbol => Array<Message>}]
109
- # @return [String]
110
- def render_zones(zones)
111
- %i[eviction middle recent].flat_map { |name|
112
- [ZONE_DELIMITERS[name], render_zone(zones[name])]
113
- }.join("\n")
114
- end
115
-
116
- # Determines which zone an event belongs to based on cumulative token position.
117
- #
118
- # @param cumulative [Numeric] cumulative token count including this event
119
- # @param zone_size [Float] token count per zone (total / 3)
120
- # @return [Symbol] :eviction, :middle, or :recent
121
- def zone_for_cumulative(cumulative, zone_size)
122
- if cumulative <= zone_size
123
- :eviction
124
- elsif cumulative <= zone_size * 2
125
- :middle
126
- else
127
- :recent
128
- end
129
- end
130
-
131
- # Renders a single zone: conversation messages as full text, consecutive
132
- # tool calls/responses compressed into `[N tools called]` counters.
133
- # tool_response messages are intentionally silent — they affect zone boundaries
134
- # via token cost but are not rendered; only tool_call messages increment the counter.
135
- #
136
- # @param zone_messages [Array<Message>]
137
- # @return [String]
138
- def render_zone(zone_messages)
139
- lines = []
140
- tool_count = 0
141
-
142
- zone_messages.each do |message|
143
- if conversation_message?(message) || think_message?(message)
144
- lines << flush_tool_count(tool_count)
145
- tool_count = 0
146
- lines << render_message_line(message)
147
- elsif message.message_type == "tool_call"
148
- tool_count += 1
149
- end
150
- end
151
-
152
- lines << flush_tool_count(tool_count)
153
- lines.compact.join("\n")
154
- end
155
-
156
- # @return [Boolean] true if message is a user/agent/system message
157
- def conversation_message?(message)
158
- message.message_type.in?(Message::CONVERSATION_TYPES)
159
- end
160
-
161
- # Think messages are tool_call messages with tool_name == "think".
162
- # They carry the agent's reasoning and are treated as conversation.
163
- #
164
- # @return [Boolean]
165
- def think_message?(message)
166
- message.message_type == "tool_call" && message.payload["tool_name"] == Message::THINK_TOOL
167
- end
168
-
169
- ROLE_LABELS = {
170
- "user_message" => "User",
171
- "agent_message" => "Assistant",
172
- "system_message" => "System"
173
- }.freeze
174
-
175
- # Renders a single message as a transcript line.
176
- #
177
- # @param message [Message]
178
- # @return [String]
179
- def render_message_line(message)
180
- prefix = "message #{message.id}"
181
- data = message.payload
182
- if think_message?(message)
183
- "#{prefix} Think: #{data.dig("tool_input", "thoughts")}"
184
- else
185
- "#{prefix} #{ROLE_LABELS.fetch(message.message_type)}: #{data["content"]}"
186
- end
187
- end
188
-
189
- # Returns a tool count string if any tools were called, nil otherwise.
190
- #
191
- # @param count [Integer] number of tool calls to flush
192
- # @return [String, nil]
193
- def flush_tool_count(count)
194
- return if count == 0
195
- "[#{count} #{(count == 1) ? "tool" : "tools"} called]"
196
- end
197
-
198
- # @return [Integer] token cost using cached count or heuristic
199
- def message_token_cost(message)
200
- cached = message.token_count
201
- (cached > 0) ? cached : message.estimate_tokens
202
- end
203
- end
204
- end
@@ -1,138 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mneme
4
- # Passive recall — automatic memory surfacing triggered by Goal updates.
5
- # When goals are created or updated, searches message history for related
6
- # context and enqueues phantom tool_call/tool_response pairs via the
7
- # PendingMessage pipeline.
8
- #
9
- # Phantom pairs are promoted into real Message records by
10
- # {Session#promote_pending_messages!} between agent loop rounds, then
11
- # ride the conveyor belt like regular messages — cached as part of the
12
- # stable prefix, compressed by Mneme on eviction.
13
- #
14
- # @example Trigger after a goal update
15
- # Mneme::PassiveRecall.new(session).call
16
- class PassiveRecall
17
- # Estimated token overhead for a tool_use wrapper (name + input fields).
18
- TOOL_PAIR_OVERHEAD_TOKENS = 50
19
-
20
- # @param session [Session] the session whose goals drive recall
21
- def initialize(session)
22
- @session = session
23
- end
24
-
25
- # Searches message history using active goal descriptions as queries.
26
- # Enqueues phantom recall pairs for new results not already recalled.
27
- #
28
- # @return [Integer] number of pending messages created
29
- def call
30
- goals = @session.goals.active.root.includes(:sub_goals)
31
- return 0 if goals.empty?
32
-
33
- search_terms = build_search_terms(goals)
34
- return 0 if search_terms.blank?
35
-
36
- results = Mneme::Search.query(search_terms, limit: Anima::Settings.recall_max_results)
37
- results = filter_duplicates(results)
38
-
39
- enqueue_pending_messages(results)
40
- end
41
-
42
- private
43
-
44
- STOP_WORDS = Set.new(%w[
45
- a an the is are was were be been being do does did
46
- have has had in on at to for of and or but not with
47
- this that it its by from as up out if about into
48
- fix add create update remove implement check set get
49
- ]).freeze
50
-
51
- # Extracts meaningful keywords from active goals and joins with OR.
52
- #
53
- # @param goals [ActiveRecord::Relation<Goal>]
54
- # @return [String] FTS5 OR-joined keywords
55
- def build_search_terms(goals)
56
- descriptions = goals.flat_map { |goal|
57
- [goal.description] + goal.sub_goals.reject(&:completed?).map(&:description)
58
- }
59
-
60
- words = descriptions.join(" ")
61
- .gsub(/[^a-zA-Z0-9\s-]/, "")
62
- .downcase
63
- .split
64
- .uniq
65
- .reject { |word| STOP_WORDS.include?(word) || word.length < 3 }
66
-
67
- words.join(" OR ").truncate(500)
68
- end
69
-
70
- # Excludes results already in the viewport or already recalled (pending or promoted).
71
- #
72
- # @param results [Array<Mneme::Search::Result>]
73
- # @return [Array<Mneme::Search::Result>]
74
- def filter_duplicates(results)
75
- viewport_ids = @session.viewport_message_ids.to_set
76
-
77
- existing_recall_ids = @session.messages
78
- .where(message_type: "tool_call")
79
- .where("payload ->> 'tool_name' = ?", PendingMessage::RECALL_MEMORY_TOOL)
80
- .pluck(:tool_use_id)
81
- .to_set
82
-
83
- pending_recall_ids = @session.pending_messages
84
- .where(source_type: "recall")
85
- .pluck(:source_name)
86
- .map { |name| "recall_#{name}" }
87
- .to_set
88
-
89
- known_ids = existing_recall_ids | pending_recall_ids
90
-
91
- results.reject { |result|
92
- viewport_ids.include?(result.message_id) ||
93
- known_ids.include?("recall_#{result.message_id}")
94
- }
95
- end
96
-
97
- # Creates PendingMessages for each recall result.
98
- #
99
- # @param results [Array<Mneme::Search::Result>]
100
- # @return [Integer] number of pending messages created
101
- def enqueue_pending_messages(results)
102
- messages_by_id = Message.where(id: results.map(&:message_id))
103
- .includes(:session).index_by(&:id)
104
-
105
- count = 0
106
- remaining = (Anima::Settings.token_budget * Anima::Settings.recall_budget_fraction).to_i
107
-
108
- results.each do |result|
109
- snippet = format_snippet(result, messages_by_id)
110
- cost = Message.estimate_token_count(snippet.bytesize) + TOOL_PAIR_OVERHEAD_TOKENS
111
- break if cost > remaining && count > 0
112
-
113
- @session.pending_messages.create!(
114
- content: snippet,
115
- source_type: "recall",
116
- source_name: result.message_id.to_s
117
- )
118
-
119
- remaining -= cost
120
- count += 1
121
- end
122
-
123
- count
124
- end
125
-
126
- # Formats a search result as a compact snippet.
127
- #
128
- # @param result [Mneme::Search::Result]
129
- # @param messages_by_id [Hash{Integer => Message}] pre-fetched messages
130
- # @return [String]
131
- def format_snippet(result, messages_by_id)
132
- msg = messages_by_id[result.message_id]
133
- session_label = msg&.session&.name || "session ##{result.session_id}"
134
- content = result.snippet.truncate(Anima::Settings.recall_max_snippet_tokens * Message::BYTES_PER_TOKEN)
135
- "message #{result.message_id} (#{session_label}): #{content}"
136
- end
137
- end
138
- end