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.
- checksums.yaml +4 -4
- data/.reek.yml +18 -20
- data/README.md +61 -95
- data/agents/thoughts-analyzer.md +12 -7
- data/anima-core.gemspec +1 -0
- data/app/channels/session_channel.rb +38 -58
- data/app/decorators/agent_message_decorator.rb +7 -2
- data/app/decorators/message_decorator.rb +31 -100
- data/app/decorators/pending_from_melete_decorator.rb +36 -0
- data/app/decorators/pending_from_melete_goal_decorator.rb +13 -0
- data/app/decorators/pending_from_melete_skill_decorator.rb +19 -0
- data/app/decorators/pending_from_melete_workflow_decorator.rb +13 -0
- data/app/decorators/pending_from_mneme_decorator.rb +44 -0
- data/app/decorators/pending_message_decorator.rb +94 -0
- data/app/decorators/pending_subagent_decorator.rb +46 -0
- data/app/decorators/pending_tool_response_decorator.rb +51 -0
- data/app/decorators/pending_user_message_decorator.rb +22 -0
- data/app/decorators/system_message_decorator.rb +5 -0
- data/app/decorators/tool_call_decorator.rb +13 -2
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +7 -2
- data/app/jobs/count_tokens_job.rb +23 -0
- data/app/jobs/drain_job.rb +169 -0
- data/app/jobs/melete_enrichment_job/goal_change_listener.rb +52 -0
- data/app/jobs/melete_enrichment_job.rb +48 -0
- data/app/jobs/mneme_enrichment_job.rb +46 -0
- data/app/jobs/tool_execution_job.rb +87 -0
- data/app/models/concerns/token_estimation.rb +54 -0
- data/app/models/goal.rb +21 -10
- data/app/models/message.rb +47 -36
- data/app/models/pending_message.rb +276 -29
- data/app/models/pinned_message.rb +8 -3
- data/app/models/session.rb +468 -432
- data/app/models/snapshot.rb +11 -21
- data/bin/inspect-cassette +17 -4
- data/config/application.rb +1 -0
- data/config/initializers/event_subscribers.rb +71 -4
- data/config/initializers/inflections.rb +3 -1
- data/db/cable_structure.sql +3 -3
- data/db/migrate/20260407170803_remove_viewport_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260407180400_remove_mneme_snapshot_pointer_columns_from_sessions.rb +6 -0
- data/db/migrate/20260411120553_add_token_count_to_pinned_messages.rb +5 -0
- data/db/migrate/20260411172926_remove_active_skills_and_workflow_from_sessions.rb +6 -0
- data/db/migrate/20260412110625_replace_processing_with_aasm_state.rb +6 -0
- data/db/migrate/20260418150323_add_kind_and_message_type_to_pending_messages.rb +6 -0
- data/db/migrate/20260419120000_add_drain_fields_to_pending_messages.rb +7 -0
- data/db/migrate/20260419130000_drop_pending_messages_kind_default.rb +5 -0
- data/db/migrate/20260419140000_add_drain_indexes_to_pending_messages.rb +8 -0
- data/db/migrate/20260420100000_add_hud_visibility_to_sessions.rb +15 -0
- data/db/queue_structure.sql +13 -13
- data/db/structure.sql +44 -31
- data/lib/agents/registry.rb +1 -1
- data/lib/anima/settings.rb +7 -33
- data/lib/anima/version.rb +1 -1
- data/lib/events/authentication_required.rb +24 -0
- data/lib/events/bounce_back.rb +4 -4
- data/lib/events/eviction_completed.rb +28 -0
- data/lib/events/goal_created.rb +28 -0
- data/lib/events/goal_updated.rb +32 -0
- data/lib/events/llm_responded.rb +35 -0
- data/lib/events/message_created.rb +27 -0
- data/lib/events/message_updated.rb +25 -0
- data/lib/events/session_state_changed.rb +30 -0
- data/lib/events/skill_activated.rb +28 -0
- data/lib/events/start_melete.rb +36 -0
- data/lib/events/start_mneme.rb +33 -0
- data/lib/events/start_processing.rb +32 -0
- data/lib/events/subagent_evicted.rb +31 -0
- data/lib/events/subscribers/active_state_broadcaster.rb +27 -0
- data/lib/events/subscribers/authentication_broadcaster.rb +34 -0
- data/lib/events/subscribers/drain_kickoff.rb +20 -0
- data/lib/events/subscribers/eviction_broadcaster.rb +26 -0
- data/lib/events/subscribers/llm_response_handler.rb +111 -0
- data/lib/events/subscribers/melete_kickoff.rb +24 -0
- data/lib/events/subscribers/message_broadcaster.rb +34 -0
- data/lib/events/subscribers/mneme_kickoff.rb +24 -0
- data/lib/events/subscribers/mneme_scheduler.rb +21 -0
- data/lib/events/subscribers/persister.rb +6 -8
- data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
- data/lib/events/subscribers/subagent_message_router.rb +26 -29
- data/lib/events/subscribers/subagent_visibility_broadcaster.rb +33 -0
- data/lib/events/subscribers/tool_response_creator.rb +33 -0
- data/lib/events/subscribers/transient_broadcaster.rb +1 -1
- data/lib/events/tool_executed.rb +34 -0
- data/lib/events/workflow_activated.rb +27 -0
- data/lib/llm/client.rb +41 -201
- data/lib/mcp/client_manager.rb +41 -46
- data/lib/mcp/stdio_transport.rb +9 -5
- data/lib/{analytical_brain → melete}/runner.rb +63 -68
- data/lib/{analytical_brain → melete}/tools/activate_skill.rb +1 -1
- data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/finish_goal.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/goal_messaging.rb +4 -3
- data/lib/{analytical_brain → melete}/tools/read_workflow.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/set_goal.rb +1 -1
- data/lib/{analytical_brain → melete}/tools/update_goal.rb +4 -4
- data/lib/{analytical_brain.rb → melete.rb} +6 -3
- data/lib/mneme/base_runner.rb +121 -0
- data/lib/mneme/l2_runner.rb +14 -20
- data/lib/mneme/recall_runner.rb +132 -0
- data/lib/mneme/runner.rb +118 -171
- data/lib/mneme/search.rb +104 -62
- data/lib/mneme/tools/nothing_to_surface.rb +25 -0
- data/lib/mneme/tools/save_snapshot.rb +2 -10
- data/lib/mneme/tools/surface_memory.rb +89 -0
- data/lib/mneme.rb +11 -5
- data/lib/shell_session.rb +287 -612
- data/lib/skills/definition.rb +2 -2
- data/lib/skills/registry.rb +1 -1
- data/lib/tools/base.rb +16 -0
- data/lib/tools/bash.rb +25 -57
- data/lib/tools/edit.rb +2 -0
- data/lib/tools/read.rb +2 -0
- data/lib/tools/registry.rb +79 -3
- data/lib/tools/{recall.rb → search_messages.rb} +19 -21
- data/lib/tools/spawn_specialist.rb +16 -10
- data/lib/tools/spawn_subagent.rb +20 -14
- data/lib/tools/subagent_prompts.rb +4 -4
- data/lib/tools/think.rb +1 -1
- data/lib/tools/{remember.rb → view_messages.rb} +10 -10
- data/lib/tools/write.rb +2 -0
- data/lib/tui/app.rb +5 -4
- data/lib/tui/braille_spinner.rb +7 -7
- data/lib/tui/decorators/base_decorator.rb +24 -3
- data/lib/tui/message_store.rb +93 -44
- data/lib/tui/screens/chat.rb +94 -20
- data/lib/tui/settings.rb +9 -2
- data/lib/workflows/definition.rb +3 -3
- data/lib/workflows/registry.rb +1 -1
- data/skills/github.md +38 -0
- data/templates/config.toml +4 -23
- data/workflows/review_pr.md +18 -14
- metadata +86 -28
- data/app/jobs/agent_request_job.rb +0 -199
- data/app/jobs/analytical_brain_job.rb +0 -33
- data/app/jobs/count_message_tokens_job.rb +0 -39
- data/app/jobs/passive_recall_job.rb +0 -24
- data/app/models/concerns/message/broadcasting.rb +0 -86
- data/lib/agent_loop.rb +0 -215
- data/lib/analytical_brain/tools/deactivate_skill.rb +0 -40
- data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -35
- data/lib/events/agent_message.rb +0 -25
- data/lib/events/subscribers/message_collector.rb +0 -64
- data/lib/events/tool_call.rb +0 -31
- data/lib/events/tool_response.rb +0 -33
- data/lib/mneme/compressed_viewport.rb +0 -204
- 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
|
data/lib/mneme/passive_recall.rb
DELETED
|
@@ -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
|