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.
- checksums.yaml +4 -4
- data/.reek.yml +10 -1
- data/README.md +36 -11
- data/agents/codebase-analyzer.md +2 -2
- data/agents/codebase-pattern-finder.md +2 -2
- data/agents/documentation-researcher.md +2 -2
- data/agents/thoughts-analyzer.md +2 -2
- data/agents/web-search-researcher.md +3 -3
- data/app/channels/session_channel.rb +83 -64
- data/app/decorators/agent_message_decorator.rb +2 -2
- data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
- data/app/decorators/system_message_decorator.rb +2 -2
- data/app/decorators/tool_call_decorator.rb +6 -6
- data/app/decorators/tool_decorator.rb +4 -4
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +5 -19
- data/app/decorators/web_get_tool_decorator.rb +41 -9
- data/app/jobs/agent_request_job.rb +33 -24
- data/app/jobs/count_message_tokens_job.rb +39 -0
- data/app/jobs/passive_recall_job.rb +4 -4
- data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
- data/app/models/goal.rb +17 -4
- data/app/models/goal_pinned_message.rb +11 -0
- data/app/models/message.rb +127 -0
- data/app/models/pending_message.rb +43 -0
- data/app/models/pinned_message.rb +41 -0
- data/app/models/secret.rb +72 -0
- data/app/models/session.rb +385 -226
- data/app/models/snapshot.rb +25 -25
- data/config/environments/test.rb +5 -0
- data/config/initializers/time_nanoseconds.rb +11 -0
- data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
- data/db/migrate/20260328100000_create_secrets.rb +15 -0
- data/db/migrate/20260328152142_add_evicted_at_to_goals.rb +6 -0
- data/db/migrate/20260329120000_create_pending_messages.rb +11 -0
- data/lib/agent_loop.rb +14 -41
- data/lib/agents/definition.rb +1 -1
- data/lib/analytical_brain/runner.rb +40 -37
- data/lib/analytical_brain/tools/activate_skill.rb +5 -9
- data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
- data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
- data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
- data/lib/analytical_brain/tools/finish_goal.rb +5 -8
- data/lib/analytical_brain/tools/read_workflow.rb +5 -9
- data/lib/analytical_brain/tools/rename_session.rb +3 -10
- data/lib/analytical_brain/tools/set_goal.rb +3 -7
- data/lib/analytical_brain/tools/update_goal.rb +3 -7
- data/lib/anima/cli/mcp/secrets.rb +4 -4
- data/lib/anima/cli/mcp.rb +4 -4
- data/lib/anima/installer.rb +7 -1
- data/lib/anima/settings.rb +46 -6
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +1 -1
- data/lib/credential_store.rb +17 -66
- data/lib/events/base.rb +1 -1
- data/lib/events/bounce_back.rb +7 -7
- data/lib/events/subscribers/persister.rb +15 -22
- data/lib/events/subscribers/subagent_message_router.rb +20 -8
- data/lib/events/subscribers/transient_broadcaster.rb +2 -2
- data/lib/events/user_message.rb +2 -13
- data/lib/llm/client.rb +54 -20
- data/lib/mcp/config.rb +2 -2
- data/lib/mcp/secrets.rb +7 -8
- data/lib/mneme/compressed_viewport.rb +57 -57
- data/lib/mneme/l2_runner.rb +4 -4
- data/lib/mneme/passive_recall.rb +2 -2
- data/lib/mneme/runner.rb +57 -75
- data/lib/mneme/search.rb +38 -38
- data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
- data/lib/mneme/tools/everything_ok.rb +1 -3
- data/lib/mneme/tools/save_snapshot.rb +12 -16
- data/lib/shell_session.rb +54 -16
- data/lib/tools/base.rb +23 -0
- data/lib/tools/bash.rb +60 -16
- data/lib/tools/edit.rb +6 -8
- data/lib/tools/mark_goal_completed.rb +86 -0
- data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
- data/lib/tools/read.rb +6 -5
- data/lib/tools/recall.rb +98 -0
- data/lib/tools/registry.rb +37 -8
- data/lib/tools/remember.rb +46 -55
- data/lib/tools/response_truncator.rb +70 -0
- data/lib/tools/spawn_specialist.rb +15 -25
- data/lib/tools/spawn_subagent.rb +14 -22
- data/lib/tools/subagent_prompts.rb +42 -6
- data/lib/tools/think.rb +26 -10
- data/lib/tools/web_get.rb +23 -4
- data/lib/tools/write.rb +4 -4
- data/lib/tui/app.rb +178 -13
- data/lib/tui/braille_spinner.rb +152 -0
- data/lib/tui/cable_client.rb +4 -4
- data/lib/tui/decorators/base_decorator.rb +17 -8
- data/lib/tui/decorators/bash_decorator.rb +2 -2
- data/lib/tui/decorators/edit_decorator.rb +5 -4
- data/lib/tui/decorators/read_decorator.rb +4 -8
- data/lib/tui/decorators/think_decorator.rb +3 -5
- data/lib/tui/decorators/web_get_decorator.rb +4 -3
- data/lib/tui/decorators/write_decorator.rb +5 -4
- data/lib/tui/flash.rb +1 -1
- data/lib/tui/formatting.rb +22 -0
- data/lib/tui/message_store.rb +103 -59
- data/lib/tui/screens/chat.rb +293 -78
- data/skills/activerecord/SKILL.md +1 -1
- data/skills/dragonruby/SKILL.md +1 -1
- data/skills/draper-decorators/SKILL.md +1 -1
- data/skills/gh-issue.md +1 -1
- data/skills/mcp-server/SKILL.md +1 -1
- data/skills/ratatui-ruby/SKILL.md +1 -1
- data/skills/rspec/SKILL.md +1 -1
- data/templates/config.toml +42 -5
- data/templates/soul.md +7 -19
- data/workflows/create_handoff.md +1 -1
- data/workflows/create_note.md +1 -1
- data/workflows/create_plan.md +1 -1
- data/workflows/implement_plan.md +1 -1
- data/workflows/iterate_plan.md +1 -1
- data/workflows/research_codebase.md +1 -1
- data/workflows/resume_handoff.md +1 -1
- data/workflows/review_pr.md +78 -16
- data/workflows/thoughts_init.md +1 -1
- data/workflows/validate_plan.md +1 -1
- metadata +20 -9
- data/app/jobs/count_event_tokens_job.rb +0 -39
- data/app/models/event.rb +0 -129
- data/app/models/goal_pinned_event.rb +0 -11
- data/app/models/pinned_event.rb +0 -41
- 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
|
data/app/models/pinned_event.rb
DELETED
|
@@ -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
|