anima-core 1.1.3 → 1.3.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +10 -1
  3. data/README.md +36 -11
  4. data/agents/codebase-analyzer.md +2 -2
  5. data/agents/codebase-pattern-finder.md +2 -2
  6. data/agents/documentation-researcher.md +2 -2
  7. data/agents/thoughts-analyzer.md +2 -2
  8. data/agents/web-search-researcher.md +3 -3
  9. data/app/channels/session_channel.rb +83 -64
  10. data/app/decorators/agent_message_decorator.rb +2 -2
  11. data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
  12. data/app/decorators/system_message_decorator.rb +2 -2
  13. data/app/decorators/tool_call_decorator.rb +6 -6
  14. data/app/decorators/tool_decorator.rb +4 -4
  15. data/app/decorators/tool_response_decorator.rb +2 -2
  16. data/app/decorators/user_message_decorator.rb +5 -19
  17. data/app/decorators/web_get_tool_decorator.rb +41 -9
  18. data/app/jobs/agent_request_job.rb +33 -24
  19. data/app/jobs/count_message_tokens_job.rb +39 -0
  20. data/app/jobs/passive_recall_job.rb +4 -4
  21. data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
  22. data/app/models/goal.rb +17 -4
  23. data/app/models/goal_pinned_message.rb +11 -0
  24. data/app/models/message.rb +127 -0
  25. data/app/models/pending_message.rb +43 -0
  26. data/app/models/pinned_message.rb +41 -0
  27. data/app/models/secret.rb +72 -0
  28. data/app/models/session.rb +385 -226
  29. data/app/models/snapshot.rb +25 -25
  30. data/config/environments/test.rb +5 -0
  31. data/config/initializers/time_nanoseconds.rb +11 -0
  32. data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
  33. data/db/migrate/20260328100000_create_secrets.rb +15 -0
  34. data/db/migrate/20260328152142_add_evicted_at_to_goals.rb +6 -0
  35. data/db/migrate/20260329120000_create_pending_messages.rb +11 -0
  36. data/lib/agent_loop.rb +14 -41
  37. data/lib/agents/definition.rb +1 -1
  38. data/lib/analytical_brain/runner.rb +40 -37
  39. data/lib/analytical_brain/tools/activate_skill.rb +5 -9
  40. data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
  41. data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
  42. data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
  43. data/lib/analytical_brain/tools/finish_goal.rb +5 -8
  44. data/lib/analytical_brain/tools/read_workflow.rb +5 -9
  45. data/lib/analytical_brain/tools/rename_session.rb +3 -10
  46. data/lib/analytical_brain/tools/set_goal.rb +3 -7
  47. data/lib/analytical_brain/tools/update_goal.rb +3 -7
  48. data/lib/anima/cli/mcp/secrets.rb +4 -4
  49. data/lib/anima/cli/mcp.rb +4 -4
  50. data/lib/anima/installer.rb +7 -1
  51. data/lib/anima/settings.rb +46 -6
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +1 -1
  54. data/lib/credential_store.rb +17 -66
  55. data/lib/events/base.rb +1 -1
  56. data/lib/events/bounce_back.rb +7 -7
  57. data/lib/events/subscribers/persister.rb +15 -22
  58. data/lib/events/subscribers/subagent_message_router.rb +20 -8
  59. data/lib/events/subscribers/transient_broadcaster.rb +2 -2
  60. data/lib/events/user_message.rb +2 -13
  61. data/lib/llm/client.rb +54 -20
  62. data/lib/mcp/config.rb +2 -2
  63. data/lib/mcp/secrets.rb +7 -8
  64. data/lib/mneme/compressed_viewport.rb +57 -57
  65. data/lib/mneme/l2_runner.rb +4 -4
  66. data/lib/mneme/passive_recall.rb +2 -2
  67. data/lib/mneme/runner.rb +57 -75
  68. data/lib/mneme/search.rb +38 -38
  69. data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
  70. data/lib/mneme/tools/everything_ok.rb +1 -3
  71. data/lib/mneme/tools/save_snapshot.rb +12 -16
  72. data/lib/shell_session.rb +54 -16
  73. data/lib/tools/base.rb +23 -0
  74. data/lib/tools/bash.rb +60 -16
  75. data/lib/tools/edit.rb +6 -8
  76. data/lib/tools/mark_goal_completed.rb +86 -0
  77. data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
  78. data/lib/tools/read.rb +6 -5
  79. data/lib/tools/recall.rb +98 -0
  80. data/lib/tools/registry.rb +37 -8
  81. data/lib/tools/remember.rb +46 -55
  82. data/lib/tools/response_truncator.rb +70 -0
  83. data/lib/tools/spawn_specialist.rb +15 -25
  84. data/lib/tools/spawn_subagent.rb +14 -22
  85. data/lib/tools/subagent_prompts.rb +42 -6
  86. data/lib/tools/think.rb +26 -10
  87. data/lib/tools/web_get.rb +23 -4
  88. data/lib/tools/write.rb +4 -4
  89. data/lib/tui/app.rb +178 -13
  90. data/lib/tui/braille_spinner.rb +152 -0
  91. data/lib/tui/cable_client.rb +4 -4
  92. data/lib/tui/decorators/base_decorator.rb +17 -8
  93. data/lib/tui/decorators/bash_decorator.rb +2 -2
  94. data/lib/tui/decorators/edit_decorator.rb +5 -4
  95. data/lib/tui/decorators/read_decorator.rb +4 -8
  96. data/lib/tui/decorators/think_decorator.rb +3 -5
  97. data/lib/tui/decorators/web_get_decorator.rb +4 -3
  98. data/lib/tui/decorators/write_decorator.rb +5 -4
  99. data/lib/tui/flash.rb +1 -1
  100. data/lib/tui/formatting.rb +22 -0
  101. data/lib/tui/message_store.rb +103 -59
  102. data/lib/tui/screens/chat.rb +293 -78
  103. data/skills/activerecord/SKILL.md +1 -1
  104. data/skills/dragonruby/SKILL.md +1 -1
  105. data/skills/draper-decorators/SKILL.md +1 -1
  106. data/skills/gh-issue.md +1 -1
  107. data/skills/mcp-server/SKILL.md +1 -1
  108. data/skills/ratatui-ruby/SKILL.md +1 -1
  109. data/skills/rspec/SKILL.md +1 -1
  110. data/templates/config.toml +42 -5
  111. data/templates/soul.md +7 -19
  112. data/workflows/create_handoff.md +1 -1
  113. data/workflows/create_note.md +1 -1
  114. data/workflows/create_plan.md +1 -1
  115. data/workflows/implement_plan.md +1 -1
  116. data/workflows/iterate_plan.md +1 -1
  117. data/workflows/research_codebase.md +1 -1
  118. data/workflows/resume_handoff.md +1 -1
  119. data/workflows/review_pr.md +78 -16
  120. data/workflows/thoughts_init.md +1 -1
  121. data/workflows/validate_plan.md +1 -1
  122. metadata +20 -9
  123. data/app/jobs/count_event_tokens_job.rb +0 -39
  124. data/app/models/event.rb +0 -129
  125. data/app/models/goal_pinned_event.rb +0 -11
  126. data/app/models/pinned_event.rb +0 -41
  127. data/lib/mneme/tools/attach_events_to_goals.rb +0 -107
data/app/models/event.rb DELETED
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # A persisted record of something that happened during a session.
4
- # Events are the single source of truth for conversation history —
5
- # there is no separate chat log, only events attached to a session.
6
- #
7
- # @!attribute event_type
8
- # @return [String] one of {TYPES}: system_message, user_message,
9
- # agent_message, tool_call, tool_response
10
- # @!attribute payload
11
- # @return [Hash] event-specific data (content, tool_name, tool_input, etc.)
12
- # @!attribute timestamp
13
- # @return [Integer] nanoseconds since epoch (Process::CLOCK_REALTIME)
14
- # @!attribute token_count
15
- # @return [Integer] cached token count for this event's payload (0 until counted)
16
- # @!attribute tool_use_id
17
- # @return [String] ID correlating tool_call and tool_response events
18
- # (Anthropic-assigned, or a SecureRandom.uuid fallback when the API returns nil;
19
- # required for tool_call and tool_response events)
20
- class Event < ApplicationRecord
21
- include Event::Broadcasting
22
-
23
- TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
24
- LLM_TYPES = %w[user_message agent_message].freeze
25
- CONTEXT_TYPES = %w[system_message user_message agent_message tool_call tool_response].freeze
26
- CONVERSATION_TYPES = %w[user_message agent_message system_message].freeze
27
- THINK_TOOL = "think"
28
- SPAWN_TOOLS = %w[spawn_subagent spawn_specialist].freeze
29
- PENDING_STATUS = "pending"
30
-
31
- # Event types that require a tool_use_id to pair call with response.
32
- TOOL_TYPES = %w[tool_call tool_response].freeze
33
-
34
- ROLE_MAP = {"user_message" => "user", "agent_message" => "assistant"}.freeze
35
-
36
- # Heuristic: average bytes per token for English prose.
37
- BYTES_PER_TOKEN = 4
38
-
39
- belongs_to :session
40
- has_many :pinned_events, dependent: :destroy
41
-
42
- validates :event_type, presence: true, inclusion: {in: TYPES}
43
- validates :payload, presence: true
44
- validates :timestamp, presence: true
45
- # Anthropic requires every tool_use to have a matching tool_result with the same ID
46
- validates :tool_use_id, presence: true, if: -> { event_type.in?(TOOL_TYPES) }
47
-
48
- after_create :schedule_token_count, if: :llm_message?
49
-
50
- # @!method self.llm_messages
51
- # Events that represent conversation turns sent to the LLM API.
52
- # @return [ActiveRecord::Relation]
53
- scope :llm_messages, -> { where(event_type: LLM_TYPES) }
54
-
55
- # @!method self.context_events
56
- # Events included in the LLM context window (messages + tool interactions).
57
- # @return [ActiveRecord::Relation]
58
- scope :context_events, -> { where(event_type: CONTEXT_TYPES) }
59
-
60
- # @!method self.pending
61
- # User messages queued during active agent processing, not yet sent to LLM.
62
- # @return [ActiveRecord::Relation]
63
- scope :pending, -> { where(status: PENDING_STATUS) }
64
-
65
- # @!method self.deliverable
66
- # Events eligible for LLM context (excludes pending messages).
67
- # NULL status means delivered/processed — the only excluded value is "pending".
68
- # @return [ActiveRecord::Relation]
69
- scope :deliverable, -> { where(status: nil) }
70
-
71
- # @!method self.excluding_spawn_events
72
- # Excludes spawn_subagent/spawn_specialist tool_call and tool_response events.
73
- # Used when building parent context for sub-agents — spawn events cause role
74
- # confusion because the sub-agent sees sibling spawn results and mistakes
75
- # itself for the parent.
76
- # @return [ActiveRecord::Relation]
77
- scope :excluding_spawn_events, -> {
78
- where.not("event_type IN (?) AND json_extract(payload, '$.tool_name') IN (?)",
79
- TOOL_TYPES, SPAWN_TOOLS)
80
- }
81
-
82
- # Maps event_type to the Anthropic Messages API role.
83
- # @return [String] "user" or "assistant"
84
- def api_role
85
- ROLE_MAP.fetch(event_type)
86
- end
87
-
88
- # @return [Boolean] true if this event represents an LLM conversation turn
89
- def llm_message?
90
- event_type.in?(LLM_TYPES)
91
- end
92
-
93
- # @return [Boolean] true if this event is part of the LLM context window
94
- def context_event?
95
- event_type.in?(CONTEXT_TYPES)
96
- end
97
-
98
- # @return [Boolean] true if this is a pending message not yet sent to the LLM
99
- def pending?
100
- status == PENDING_STATUS
101
- end
102
-
103
- # @return [Boolean] true if this is a conversation event (user/agent/system message)
104
- # or a think tool_call — the events Mneme treats as "conversation" for boundary tracking
105
- def conversation_or_think?
106
- event_type.in?(CONVERSATION_TYPES) ||
107
- (event_type == "tool_call" && payload["tool_name"] == THINK_TOOL)
108
- end
109
-
110
- # Heuristic token estimate: ~4 bytes per token for English prose.
111
- # Tool events are estimated from the full payload JSON since tool_input
112
- # and tool metadata contribute to token count. Messages use content only.
113
- #
114
- # @return [Integer] estimated token count (at least 1)
115
- def estimate_tokens
116
- text = if event_type.in?(TOOL_TYPES)
117
- payload.to_json
118
- else
119
- payload["content"].to_s
120
- end
121
- [(text.bytesize / BYTES_PER_TOKEN.to_f).ceil, 1].max
122
- end
123
-
124
- private
125
-
126
- def schedule_token_count
127
- CountEventTokensJob.perform_later(id)
128
- end
129
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Join record linking a {Goal} to a {PinnedEvent}. Many-to-many: one event
4
- # can be pinned to multiple Goals, and one Goal can reference multiple pins.
5
- # When the last Goal referencing a pin completes, the pin is released.
6
- class GoalPinnedEvent < ApplicationRecord
7
- belongs_to :goal
8
- belongs_to :pinned_event
9
-
10
- validates :pinned_event_id, uniqueness: {scope: :goal_id}
11
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # A conversation event pinned to one or more Goals by Mneme to protect it
4
- # from viewport eviction. Pinned events appear in the Goals section of
5
- # the viewport, giving the main agent access to critical context that
6
- # would otherwise scroll out of the sliding window.
7
- #
8
- # Pinning is goal-scoped: when all Goals referencing a pin complete,
9
- # the pin is automatically released (reference-counted cleanup).
10
- #
11
- # @!attribute display_text
12
- # @return [String] truncated event content (~200 chars) shown in the Goals section
13
- class PinnedEvent < ApplicationRecord
14
- # Display text limit — enough to recognize content, cheap on tokens.
15
- MAX_DISPLAY_TEXT_LENGTH = 200
16
-
17
- belongs_to :event
18
-
19
- has_many :goal_pinned_events, dependent: :destroy
20
- has_many :goals, through: :goal_pinned_events
21
-
22
- validates :display_text, presence: true, length: {maximum: MAX_DISPLAY_TEXT_LENGTH}
23
- validates :event_id, uniqueness: true
24
-
25
- # Pinned events with no remaining active goals — safe to release.
26
- #
27
- # @return [ActiveRecord::Relation]
28
- scope :orphaned, -> {
29
- where.not(
30
- "EXISTS (SELECT 1 FROM goal_pinned_events gpe " \
31
- "JOIN goals ON goals.id = gpe.goal_id " \
32
- "WHERE gpe.pinned_event_id = pinned_events.id " \
33
- "AND goals.status = 'active')"
34
- )
35
- }
36
-
37
- # @return [Integer] token cost estimate for viewport budget accounting
38
- def token_cost
39
- [(display_text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
40
- end
41
- end
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mneme
4
- module Tools
5
- # Pins critical events to active Goals so they survive viewport eviction.
6
- # Mneme calls this when it sees important events (user instructions, key
7
- # decisions, critical corrections) approaching the eviction zone.
8
- #
9
- # Events are pinned via a many-to-many join: one event can be attached
10
- # to multiple Goals. When all referencing Goals complete, the pin is
11
- # automatically released (reference-counted cleanup in {Goal#release_orphaned_pins!}).
12
- class AttachEventsToGoals < ::Tools::Base
13
- def self.tool_name = "attach_events_to_goals"
14
-
15
- def self.description = "Pin critical events to active goals so they survive " \
16
- "viewport eviction. Use this for events that are too important to lose — " \
17
- "exact user instructions, key decisions, critical corrections. " \
18
- "Events stay pinned until all attached goals complete."
19
-
20
- def self.input_schema
21
- {
22
- type: "object",
23
- properties: {
24
- event_ids: {
25
- type: "array",
26
- items: {type: "integer"},
27
- description: "Database IDs of events to pin (from `event N` prefixes in the viewport)"
28
- },
29
- goal_ids: {
30
- type: "array",
31
- items: {type: "integer"},
32
- description: "IDs of active goals to attach the events to"
33
- }
34
- },
35
- required: %w[event_ids goal_ids]
36
- }
37
- end
38
-
39
- # @param main_session [Session] the session being observed
40
- def initialize(main_session:, **)
41
- @session = main_session
42
- end
43
-
44
- # @param input [Hash<String, Object>] with "event_ids" and "goal_ids"
45
- # @return [String] confirmation with link count, or error description
46
- def execute(input)
47
- event_ids = Array(input["event_ids"]).map(&:to_i).uniq
48
- goal_ids = Array(input["goal_ids"]).map(&:to_i).uniq
49
-
50
- return "Error: event_ids cannot be empty" if event_ids.empty?
51
- return "Error: goal_ids cannot be empty" if goal_ids.empty?
52
-
53
- events = @session.events.where(id: event_ids)
54
- goals = @session.goals.active.where(id: goal_ids)
55
-
56
- missing_events = event_ids - events.pluck(:id)
57
- inactive_goal_ids = goal_ids - goals.pluck(:id)
58
-
59
- errors = []
60
- errors << "Events not found: #{missing_events.join(", ")}" if missing_events.any?
61
-
62
- if inactive_goal_ids.any?
63
- completed_ids = @session.goals.completed.where(id: inactive_goal_ids).pluck(:id)
64
- not_found_ids = inactive_goal_ids - completed_ids
65
- errors << "Goals already completed: #{completed_ids.join(", ")}" if completed_ids.any?
66
- errors << "Goals not found: #{not_found_ids.join(", ")}" if not_found_ids.any?
67
- end
68
-
69
- return "Error: #{errors.join("; ")}" if errors.any?
70
-
71
- attached = attach(events, goals)
72
- "Pinned #{attached} event-goal links"
73
- end
74
-
75
- private
76
-
77
- def attach(events, goals)
78
- events.sum do |event|
79
- pinned = find_or_create_pinned_event(event)
80
- link_to_goals(pinned, goals)
81
- end
82
- end
83
-
84
- def link_to_goals(pinned, goals)
85
- goals.each { |goal| GoalPinnedEvent.find_or_create_by!(goal: goal, pinned_event: pinned) }
86
- goals.size
87
- end
88
-
89
- def find_or_create_pinned_event(event)
90
- PinnedEvent.find_or_create_by!(event: event) do |pe|
91
- pe.display_text = truncate_event_content(event)
92
- end
93
- end
94
-
95
- def truncate_event_content(event)
96
- content = event.payload&.dig("content").to_s.strip
97
- content = "event #{event.id}" if content.empty?
98
-
99
- if content.length > PinnedEvent::MAX_DISPLAY_TEXT_LENGTH
100
- content[0, PinnedEvent::MAX_DISPLAY_TEXT_LENGTH - 1] + "…"
101
- else
102
- content
103
- end
104
- end
105
- end
106
- end
107
- end