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
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "settings"
4
+
3
5
  module TUI
4
6
  # Thread-safe in-memory store for chat entries displayed in the TUI.
5
- # Replaces {Events::Subscribers::MessageCollector} in the WebSocket-based
6
- # TUI, with no dependency on Rails or the Events module.
7
+ # Holds the WebSocket-delivered view of the session's conversation with
8
+ # no dependency on Rails or the Events module.
7
9
  #
8
10
  # Accepts Action Cable message payloads and stores typed entries:
9
11
  # - `{type: :rendered, data:, message_type:, id:}` for messages with structured decorator output
@@ -40,6 +42,7 @@ module TUI
40
42
  @pending_by_id = {}
41
43
  @mutex = Mutex.new
42
44
  @version = 0
45
+ @token_economy = default_token_economy
43
46
  end
44
47
 
45
48
  # Monotonically increasing counter that bumps on every mutation.
@@ -60,6 +63,36 @@ module TUI
60
63
  @mutex.synchronize { @entries.size + @pending_entries.size }
61
64
  end
62
65
 
66
+ # Returns token economy data for HUD display.
67
+ #
68
+ # Token counts, rate limits, and cache hit rate reflect the most recent API
69
+ # call — accumulating them across a session produces values larger than the
70
+ # context window, which is meaningless. `call_count` and `cache_history`
71
+ # remain session-wide so the HUD can detect "any metrics yet?" and render
72
+ # the per-call sparkline.
73
+ #
74
+ # @return [Hash] token economy stats:
75
+ # - :input_tokens [Integer] uncached input tokens from the latest call
76
+ # - :output_tokens [Integer] output tokens from the latest call
77
+ # - :cache_read_input_tokens [Integer] cached token reads from the latest call
78
+ # - :cache_creation_input_tokens [Integer] cache writes from the latest call
79
+ # - :call_count [Integer] number of API calls tracked this session
80
+ # - :cache_hit_rate [Float] hit rate of the latest call (0.0-1.0)
81
+ # - :rate_limits [Hash, nil] latest rate limit values from API
82
+ # - :cache_history [Array<Float>] per-call hit rates for the sparkline
83
+ def token_economy
84
+ @mutex.synchronize do
85
+ stats = @token_economy.dup
86
+ total_input = stats[:input_tokens] + stats[:cache_read_input_tokens] + stats[:cache_creation_input_tokens]
87
+ stats[:cache_hit_rate] = if total_input > 0
88
+ stats[:cache_read_input_tokens].to_f / total_input
89
+ else
90
+ 0.0
91
+ end
92
+ stats
93
+ end
94
+ end
95
+
63
96
  # Processes a raw event payload from the WebSocket channel.
64
97
  # Uses structured decorator data when available; falls back to
65
98
  # role/content extraction for messages and tool counter aggregation.
@@ -67,12 +100,20 @@ module TUI
67
100
  # Events with `"action" => "update"` and a matching `"id"` replace
68
101
  # the existing entry's data in-place rather than appending.
69
102
  #
103
+ # Extracts api_metrics when present and records the latest call's
104
+ # token economy data for the HUD.
105
+ #
70
106
  # @param event_data [Hash] Action Cable event payload with "type", "content",
71
- # and optionally "rendered" (hash of mode => lines), "id", "action"
107
+ # and optionally "rendered" (hash of mode => lines), "id", "action", "api_metrics"
72
108
  # @return [Boolean] true if the event type was recognized and handled
73
109
  def process_event(event_data)
74
110
  message_id = event_data["id"]
75
111
 
112
+ # Track API metrics for token economy HUD (only on create, not update)
113
+ if event_data["action"] != "update"
114
+ track_api_metrics(event_data["api_metrics"])
115
+ end
116
+
76
117
  if event_data["action"] == "update" && message_id
77
118
  return update_existing(event_data, message_id)
78
119
  end
@@ -93,6 +134,7 @@ module TUI
93
134
 
94
135
  # Removes all entries. Called on view mode change and session switch
95
136
  # to prepare for re-decorated viewport messages from the server.
137
+ # Resets token economy totals since we're starting fresh.
96
138
  # @return [void]
97
139
  def clear
98
140
  @mutex.synchronize do
@@ -100,28 +142,44 @@ module TUI
100
142
  @entries_by_id = {}
101
143
  @pending_entries = []
102
144
  @pending_by_id = {}
145
+ @token_economy = default_token_economy
103
146
  @version += 1
104
147
  end
105
148
  end
106
149
 
107
- # Adds a pending message to the separate pending list.
150
+ # Adds a pending message to the separate pending list, or removes any
151
+ # existing entry when the decorator hides the PM in the current view
152
+ # mode (rendered hash present but its sole value is nil).
153
+ #
154
+ # Accepts the full +pending_message_created+ event payload. Falls back
155
+ # to a dimmed user-message envelope when no +rendered+ key is present
156
+ # (legacy producers, simple test fixtures).
157
+ #
108
158
  # Pending messages always render after real messages.
109
159
  #
110
160
  # @param pending_message_id [Integer] PendingMessage database ID
111
- # @param content [String] message text
161
+ # @param payload [Hash] event payload with "content", "message_type",
162
+ # and optionally "rendered" (hash of mode => decorator output)
112
163
  # @return [void]
113
- def add_pending(pending_message_id, content)
164
+ def add_pending(pending_message_id, payload)
165
+ data = pending_entry_data(payload)
166
+ message_type = payload["message_type"] || "user_message"
167
+
114
168
  @mutex.synchronize do
115
- entry = {
116
- type: :rendered,
117
- data: {"role" => "user", "content" => content, "status" => "pending"},
118
- message_type: "user_message",
119
- pending_message_id: pending_message_id
120
- }
121
- old = @pending_by_id[pending_message_id]
169
+ old = @pending_by_id.delete(pending_message_id)
122
170
  @pending_entries.delete(old) if old
123
- @pending_entries << entry
124
- @pending_by_id[pending_message_id] = entry
171
+
172
+ if data
173
+ entry = {
174
+ type: :rendered,
175
+ data: data,
176
+ message_type: message_type,
177
+ pending_message_id: pending_message_id
178
+ }
179
+ @pending_entries << entry
180
+ @pending_by_id[pending_message_id] = entry
181
+ end
182
+
125
183
  @version += 1
126
184
  end
127
185
  end
@@ -169,24 +227,21 @@ module TUI
169
227
  end
170
228
  end
171
229
 
172
- # Removes entries by their message IDs. Used when the brain reports
173
- # that messages have left the LLM's viewport (context window eviction).
174
- # Acquires the mutex once for the entire batch.
230
+ # Removes all entries with message ID <= cutoff. Used when Mneme
231
+ # evicts messages above the cutoff in the chat view (older messages
232
+ # at the top with smaller IDs).
175
233
  #
176
- # @param message_ids [Array<Integer>] database IDs of messages to remove
234
+ # @param cutoff_id [Integer] last evicted message ID
177
235
  # @return [Integer] count of entries actually removed
178
- def remove_by_ids(message_ids)
236
+ def remove_above(cutoff_id)
179
237
  @mutex.synchronize do
180
- removed = 0
181
- message_ids.each do |message_id|
182
- entry = @entries_by_id.delete(message_id)
183
- next unless entry
184
-
238
+ evicted = @entries.select { |e| e[:id] && e[:id] <= cutoff_id }
239
+ evicted.each do |entry|
185
240
  @entries.delete(entry)
186
- removed += 1
241
+ @entries_by_id.delete(entry[:id])
187
242
  end
188
- @version += 1 if removed > 0
189
- removed
243
+ @version += 1 if evicted.any?
244
+ evicted.size
190
245
  end
191
246
  end
192
247
 
@@ -211,6 +266,33 @@ module TUI
211
266
  end
212
267
  end
213
268
 
269
+ # Builds the +data+ hash for a pending entry. Returns nil when the
270
+ # decorator hid the PM (the +rendered+ key is present but its sole
271
+ # value is nil), so the caller can treat that as a removal. Falls
272
+ # back to a dimmed user-message envelope when no +rendered+ key is
273
+ # present at all (legacy callers, fixtures).
274
+ #
275
+ # The decorator's hash already carries its own +"status" => "pending"+
276
+ # marker; the symbolized form is converted here so the TUI's lookup
277
+ # via +data["status"]+ remains uniform.
278
+ def pending_entry_data(payload)
279
+ if payload.key?("rendered")
280
+ values = payload["rendered"]&.values&.compact
281
+ return nil if values.nil? || values.empty?
282
+
283
+ normalize_pending_data(values.first)
284
+ else
285
+ {"role" => "user", "content" => payload["content"], "status" => "pending"}
286
+ end
287
+ end
288
+
289
+ # Symbol/string keys arrive interchangeably from decorators that build
290
+ # hash literals server-side. Normalize keys to strings so chat.rb's
291
+ # +data["role"]+ / +data["status"]+ lookups always hit.
292
+ def normalize_pending_data(rendered)
293
+ rendered.transform_keys(&:to_s)
294
+ end
295
+
214
296
  # Extracts the first non-nil structured data hash from the rendered payload.
215
297
  # The "rendered" hash is keyed by view mode — the server includes only the
216
298
  # session's current mode, so there is always at most one entry.
@@ -346,5 +428,60 @@ module TUI
346
428
  last = @entries.last
347
429
  last if last&.dig(:type) == :tool_counter
348
430
  end
431
+
432
+ # Default token economy state for initialization and reset.
433
+ # @return [Hash]
434
+ def default_token_economy
435
+ {
436
+ input_tokens: 0,
437
+ output_tokens: 0,
438
+ cache_read_input_tokens: 0,
439
+ cache_creation_input_tokens: 0,
440
+ call_count: 0,
441
+ rate_limits: nil,
442
+ cache_history: []
443
+ }
444
+ end
445
+
446
+ # Records API metrics from the most recent message.
447
+ #
448
+ # Token counts and rate limits are last-wins (per-request semantics);
449
+ # `call_count` increments and `cache_history` appends so the HUD can
450
+ # show a sparkline of per-call hit rates.
451
+ #
452
+ # @param api_metrics [Hash, nil] metrics from API response with "usage" and "rate_limits"
453
+ # @return [void]
454
+ def track_api_metrics(api_metrics)
455
+ return unless api_metrics.is_a?(Hash)
456
+
457
+ @mutex.synchronize do
458
+ usage = api_metrics["usage"]
459
+ if usage.is_a?(Hash)
460
+ input = usage["input_tokens"].to_i
461
+ cache_read = usage["cache_read_input_tokens"].to_i
462
+ cache_create = usage["cache_creation_input_tokens"].to_i
463
+
464
+ @token_economy[:input_tokens] = input
465
+ @token_economy[:output_tokens] = usage["output_tokens"].to_i
466
+ @token_economy[:cache_read_input_tokens] = cache_read
467
+ @token_economy[:cache_creation_input_tokens] = cache_create
468
+ @token_economy[:call_count] += 1
469
+
470
+ # Per-call cache hit rate for sparkline graph
471
+ total = input + cache_read + cache_create
472
+ hit_rate = (total > 0) ? cache_read.to_f / total : 0.0
473
+ history = @token_economy[:cache_history]
474
+ history.shift if history.size >= Settings.message_store_max_cache_history
475
+ history << hit_rate
476
+ end
477
+
478
+ rate_limits = api_metrics["rate_limits"]
479
+ if rate_limits.is_a?(Hash)
480
+ @token_economy[:rate_limits] = rate_limits
481
+ end
482
+
483
+ @version += 1
484
+ end
485
+ end
349
486
  end
350
487
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "logger"
4
+ require_relative "settings"
4
5
 
5
6
  module TUI
6
7
  # Frame-level performance logger for TUI render profiling.
@@ -18,8 +19,6 @@ module TUI
18
19
  # logger.measure(:line_count) { widget.line_count(width) }
19
20
  # logger.end_frame
20
21
  class PerformanceLogger
21
- LOG_PATH = "log/tui_performance.log"
22
-
23
22
  # @param enabled [Boolean] whether to actually log (no-op when false)
24
23
  def initialize(enabled: false)
25
24
  @enabled = enabled
@@ -30,7 +29,7 @@ module TUI
30
29
 
31
30
  return unless @enabled
32
31
 
33
- @logger = Logger.new(LOG_PATH, 1, 5 * 1024 * 1024) # 5MB rotation
32
+ @logger = Logger.new(Settings.performance_log_path, 1, 5 * 1024 * 1024) # 5MB rotation
34
33
  @logger.formatter = proc { |_sev, time, _prog, msg| "#{time.strftime("%H:%M:%S.%L")} #{msg}\n" }
35
34
  @logger.info("TUI Performance Logger started — pid=#{Process.pid}")
36
35
  end