anima-core 0.2.1 → 1.0.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -12,17 +12,28 @@ class CountEventTokensJob < ApplicationJob
12
12
  # @param event_id [Integer] the Event record to count tokens for
13
13
  def perform(event_id)
14
14
  event = Event.find(event_id)
15
- return if event.token_count > 0
15
+ return if already_counted?(event)
16
16
 
17
17
  provider = Providers::Anthropic.new
18
18
  messages = [{role: event.api_role, content: event.payload["content"].to_s}]
19
19
 
20
20
  token_count = provider.count_tokens(
21
- model: LLM::Client::DEFAULT_MODEL,
21
+ model: Anima::Settings.model,
22
22
  messages: messages
23
23
  )
24
24
 
25
- # Atomic update: only write if still uncounted (avoids race with parallel jobs).
26
- Event.where(id: event.id, token_count: 0).update_all(token_count: token_count)
25
+ # Guard against parallel jobs: reload and re-check before writing.
26
+ # Uses update! (not update_all) so {Event::Broadcasting} after_update_commit
27
+ # broadcasts the updated token count to connected clients.
28
+ event.reload
29
+ return if already_counted?(event)
30
+
31
+ event.update!(token_count: token_count)
32
+ end
33
+
34
+ private
35
+
36
+ def already_counted?(event)
37
+ event.token_count > 0
27
38
  end
28
39
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Broadcasts Event records to connected WebSocket clients via ActionCable.
4
+ # Follows the Turbo Streams pattern: events are broadcast on both create
5
+ # and update, with an action type so clients can distinguish append from
6
+ # replace operations.
7
+ #
8
+ # Each broadcast includes the Event's database ID, enabling clients to
9
+ # maintain an ID-indexed store for efficient in-place updates (e.g. when
10
+ # token counts arrive asynchronously from {CountEventTokensJob}).
11
+ #
12
+ # When a new event pushes old events out of the LLM's context window,
13
+ # the broadcast includes `evicted_event_ids` so clients can remove
14
+ # phantom messages that the agent no longer knows about.
15
+ #
16
+ # @example Create broadcast payload
17
+ # {
18
+ # "type" => "user_message", "content" => "hello", ...,
19
+ # "id" => 42, "action" => "create",
20
+ # "rendered" => { "basic" => { "role" => "user", "content" => "hello" } }
21
+ # }
22
+ #
23
+ # @example Broadcast with viewport evictions
24
+ # {
25
+ # "type" => "agent_message", "content" => "...", ...,
26
+ # "id" => 99, "action" => "create",
27
+ # "evicted_event_ids" => [101, 102, 103]
28
+ # }
29
+ #
30
+ # @example Update broadcast payload (e.g. token count arrives)
31
+ # {
32
+ # "type" => "user_message", "content" => "hello", ...,
33
+ # "id" => 42, "action" => "update",
34
+ # "rendered" => { "debug" => { "role" => "user", "content" => "hello", "tokens" => 15 } }
35
+ # }
36
+ module Event::Broadcasting
37
+ extend ActiveSupport::Concern
38
+
39
+ ACTION_CREATE = "create"
40
+ ACTION_UPDATE = "update"
41
+
42
+ included do
43
+ after_create_commit :broadcast_create
44
+ after_update_commit :broadcast_update
45
+ end
46
+
47
+ private
48
+
49
+ def broadcast_create
50
+ broadcast_event(action: ACTION_CREATE)
51
+ end
52
+
53
+ def broadcast_update
54
+ broadcast_event(action: ACTION_UPDATE)
55
+ end
56
+
57
+ # Decorates the event for the session's current view mode and broadcasts
58
+ # the payload to the session's ActionCable stream. Includes viewport
59
+ # eviction metadata so clients can remove messages the LLM has forgotten.
60
+ #
61
+ # @param action [String] ACTION_CREATE or ACTION_UPDATE — tells clients how to handle the event
62
+ def broadcast_event(action:)
63
+ return unless session_id
64
+
65
+ session = Session.find_by(id: session_id)
66
+ return unless session
67
+
68
+ mode = session.view_mode
69
+ decorator = EventDecorator.for(self)
70
+ broadcast_payload = payload.merge("id" => id, "action" => action)
71
+
72
+ if decorator
73
+ broadcast_payload["rendered"] = {mode => decorator.render(mode)}
74
+ end
75
+
76
+ evicted_ids = session.recalculate_viewport!
77
+ broadcast_payload["evicted_event_ids"] = evicted_ids if evicted_ids.any?
78
+
79
+ ActionCable.server.broadcast("session_#{session_id}", broadcast_payload)
80
+ end
81
+ end
data/app/models/event.rb CHANGED
@@ -16,9 +16,12 @@
16
16
  # @!attribute tool_use_id
17
17
  # @return [String, nil] Anthropic-assigned ID correlating tool_call and tool_response
18
18
  class Event < ApplicationRecord
19
+ include Event::Broadcasting
20
+
19
21
  TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
20
22
  LLM_TYPES = %w[user_message agent_message].freeze
21
- CONTEXT_TYPES = %w[user_message agent_message tool_call tool_response].freeze
23
+ CONTEXT_TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
24
+ PENDING_STATUS = "pending"
22
25
 
23
26
  ROLE_MAP = {"user_message" => "user", "agent_message" => "assistant"}.freeze
24
27
 
@@ -43,6 +46,17 @@ class Event < ApplicationRecord
43
46
  # @return [ActiveRecord::Relation]
44
47
  scope :context_events, -> { where(event_type: CONTEXT_TYPES) }
45
48
 
49
+ # @!method self.pending
50
+ # User messages queued during active agent processing, not yet sent to LLM.
51
+ # @return [ActiveRecord::Relation]
52
+ scope :pending, -> { where(status: PENDING_STATUS) }
53
+
54
+ # @!method self.deliverable
55
+ # Events eligible for LLM context (excludes pending messages).
56
+ # NULL status means delivered/processed — the only excluded value is "pending".
57
+ # @return [ActiveRecord::Relation]
58
+ scope :deliverable, -> { where(status: nil) }
59
+
46
60
  # Maps event_type to the Anthropic Messages API role.
47
61
  # @return [String] "user" or "assistant"
48
62
  def api_role
@@ -59,6 +73,11 @@ class Event < ApplicationRecord
59
73
  event_type.in?(CONTEXT_TYPES)
60
74
  end
61
75
 
76
+ # @return [Boolean] true if this is a pending message not yet sent to the LLM
77
+ def pending?
78
+ status == PENDING_STATUS
79
+ end
80
+
62
81
  # Heuristic token estimate: ~4 bytes per token for English prose.
63
82
  # Tool events are estimated from the full payload JSON since tool_input
64
83
  # and tool metadata contribute to token count. Messages use content only.
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A persistent objective tracked by the analytical brain during a session.
4
+ # Goals form a two-level hierarchy: root goals represent high-level
5
+ # objectives (semantic episodes), while sub-goals are TODO-style steps
6
+ # rendered as checklist items in the agent's system prompt.
7
+ #
8
+ # The analytical brain creates and completes goals; the main agent sees
9
+ # them in its context window but never manages them directly.
10
+ class Goal < ApplicationRecord
11
+ STATUSES = %w[active completed].freeze
12
+
13
+ belongs_to :session
14
+ belongs_to :parent_goal, class_name: "Goal", optional: true
15
+ has_many :sub_goals, -> { order(:created_at) }, class_name: "Goal", foreign_key: :parent_goal_id, dependent: :destroy
16
+
17
+ validates :description, presence: true
18
+ validates :status, inclusion: {in: STATUSES}
19
+ validate :parent_goal_belongs_to_same_session, if: :parent_goal
20
+ validate :parent_goal_is_root, if: :parent_goal
21
+
22
+ scope :active, -> { where(status: "active") }
23
+ scope :completed, -> { where(status: "completed") }
24
+ scope :root, -> { where(parent_goal_id: nil) }
25
+
26
+ after_commit :broadcast_goals_update
27
+
28
+ # @return [Boolean] true if this goal has been completed
29
+ def completed? = status == "completed"
30
+
31
+ # @return [Boolean] true if this is a root goal (no parent)
32
+ def root? = !parent_goal_id
33
+
34
+ # Cascades completion to all active sub-goals. Called when a root goal
35
+ # is finished — remaining sub-items are implicitly resolved because
36
+ # the semantic episode that spawned them has ended.
37
+ #
38
+ # Uses +update_all+ to avoid N per-record +after_commit+ broadcasts;
39
+ # the caller ({AnalyticalBrain::Tools::FinishGoal}) wraps the whole
40
+ # operation in a transaction so the root goal's single broadcast
41
+ # includes the cascaded state.
42
+ #
43
+ # @return [void]
44
+ def cascade_completion!
45
+ now = Time.current
46
+ sub_goals.active.update_all(status: "completed", completed_at: now, updated_at: now)
47
+ end
48
+
49
+ # Serializes this goal for ActionCable broadcast and TUI display.
50
+ # Includes nested sub-goals for root goals.
51
+ #
52
+ # @return [Hash{String => Object}] with keys "id", "description", "status",
53
+ # and "sub_goals" (Array of Hash with "id", "description", "status")
54
+ def as_summary
55
+ {
56
+ "id" => id,
57
+ "description" => description,
58
+ "status" => status,
59
+ "sub_goals" => sub_goals.map { |sub|
60
+ {"id" => sub.id, "description" => sub.description, "status" => sub.status}
61
+ }
62
+ }
63
+ end
64
+
65
+ private
66
+
67
+ def parent_goal_belongs_to_same_session
68
+ return if parent_goal.session_id == session_id
69
+
70
+ errors.add(:parent_goal, "must belong to the same session")
71
+ end
72
+
73
+ def parent_goal_is_root
74
+ return unless parent_goal.parent_goal_id
75
+
76
+ errors.add(:parent_goal, "cannot nest deeper than two levels")
77
+ end
78
+
79
+ # Broadcasts goal changes to all clients subscribed to this session.
80
+ # Mirrors the Session#broadcast_active_skills_update pattern so the
81
+ # TUI info panel updates reactively.
82
+ #
83
+ # @return [void]
84
+ def broadcast_goals_update
85
+ ActionCable.server.broadcast("session_#{session_id}", {
86
+ "action" => "goals_updated",
87
+ "session_id" => session_id,
88
+ "goals" => session.goals_summary
89
+ })
90
+ end
91
+ end