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,26 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
# Orchestrates
|
|
5
|
-
#
|
|
3
|
+
module Melete
|
|
4
|
+
# Orchestrates Melete — a phantom (non-persisted) LLM loop that
|
|
5
|
+
# observes the main session and prepares skills, workflows, goals,
|
|
6
|
+
# and session names so the main agent can perform cleanly.
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
-
# modules, each contributing a prompt section and tools. Which modules
|
|
9
|
-
# active depends on the session type:
|
|
8
|
+
# Melete's capabilities are assembled from independent {Responsibility}
|
|
9
|
+
# modules, each contributing a prompt section and tools. Which modules
|
|
10
|
+
# are active depends on the session type:
|
|
10
11
|
#
|
|
11
12
|
# * **Parent sessions** — session naming, skill/workflow/goal management
|
|
12
13
|
# * **Child sessions** — sub-agent nickname assignment, skill management
|
|
13
|
-
# (goal tracking and workflows disabled — sub-agents manage their sole
|
|
14
|
-
# via mark_goal_completed)
|
|
14
|
+
# (goal tracking and workflows disabled — sub-agents manage their sole
|
|
15
|
+
# goal via mark_goal_completed)
|
|
15
16
|
#
|
|
16
|
-
# Tools mutate the observed session directly (e.g. renaming it,
|
|
17
|
-
# skills), but no trace of
|
|
18
|
-
# emitted into a phantom session (session_id: nil).
|
|
17
|
+
# Tools mutate the observed session directly (e.g. renaming it,
|
|
18
|
+
# activating skills), but no trace of Melete's reasoning is persisted —
|
|
19
|
+
# events are emitted into a phantom session (session_id: nil).
|
|
19
20
|
#
|
|
20
21
|
# @example
|
|
21
|
-
#
|
|
22
|
+
# Melete::Runner.new(session).call
|
|
22
23
|
class Runner
|
|
23
|
-
# A composable unit of
|
|
24
|
+
# A composable unit of Melete's capability: a prompt section + its tools.
|
|
24
25
|
Responsibility = Data.define(:prompt, :tools)
|
|
25
26
|
|
|
26
27
|
RESPONSIBILITIES = {
|
|
@@ -29,7 +30,7 @@ module AnalyticalBrain
|
|
|
29
30
|
──────────────────────────────
|
|
30
31
|
SESSION NAMING
|
|
31
32
|
──────────────────────────────
|
|
32
|
-
Name the session
|
|
33
|
+
Name the session once the topic becomes clear. Rename if it shifts.
|
|
33
34
|
Format: one emoji + 1-3 descriptive words.
|
|
34
35
|
PROMPT
|
|
35
36
|
tools: [Tools::RenameSession]
|
|
@@ -53,12 +54,11 @@ module AnalyticalBrain
|
|
|
53
54
|
──────────────────────────────
|
|
54
55
|
SKILL MANAGEMENT
|
|
55
56
|
──────────────────────────────
|
|
56
|
-
Activate
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Multiple skills can be active at once.
|
|
57
|
+
Activate a skill the moment the conversation signals its domain — before Aoide needs it. Late activation means she's working without the knowledge you prepared.
|
|
58
|
+
|
|
59
|
+
An activated skill rides Aoide's viewport as a message and leaves on its own when it evicts — you cannot take it back. So be careful: an irrelevant skill crowds her context with text she has to read and ignore until it falls off. Match each activation to the work actually in front of her. Multiple skills can be active at once — each one is a page she has to carry until it evicts.
|
|
60
60
|
PROMPT
|
|
61
|
-
tools: [Tools::ActivateSkill
|
|
61
|
+
tools: [Tools::ActivateSkill]
|
|
62
62
|
),
|
|
63
63
|
|
|
64
64
|
workflow_management: Responsibility.new(
|
|
@@ -66,13 +66,11 @@ module AnalyticalBrain
|
|
|
66
66
|
──────────────────────────────
|
|
67
67
|
WORKFLOW MANAGEMENT
|
|
68
68
|
──────────────────────────────
|
|
69
|
-
Activate a workflow when
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Deactivate the workflow when it completes or the user shifts focus.
|
|
73
|
-
Only one workflow active at a time — activating a new one replaces the previous.
|
|
69
|
+
Activate a workflow when Aoide starts a multi-step task that matches one. Read the returned content and use judgment to turn it into goals — not a mechanical 1:1 mapping. Adapt: skip irrelevant steps, add extra ones for unfamiliar ground.
|
|
70
|
+
|
|
71
|
+
Like skills, a workflow rides Aoide's viewport once activated and leaves when it evicts — there is no deactivation. An irrelevant or stale workflow is text Aoide carries whether she needs it or not, so only activate one when the task genuinely matches.
|
|
74
72
|
PROMPT
|
|
75
|
-
tools: [Tools::ReadWorkflow
|
|
73
|
+
tools: [Tools::ReadWorkflow]
|
|
76
74
|
),
|
|
77
75
|
|
|
78
76
|
goal_tracking: Responsibility.new(
|
|
@@ -80,29 +78,25 @@ module AnalyticalBrain
|
|
|
80
78
|
──────────────────────────────
|
|
81
79
|
GOAL TRACKING
|
|
82
80
|
──────────────────────────────
|
|
83
|
-
Create a root goal when
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Mark goals complete when the agent finishes the work they describe.
|
|
87
|
-
Completing a root goal cascades — all sub-goals are finished too.
|
|
88
|
-
Never duplicate an existing goal — check the active goals list first.
|
|
81
|
+
Create a root goal when Aoide starts a multi-step task. Break it into sub-goals as the plan takes shape. Refine wording as understanding evolves. Mark goals complete when she finishes the work they describe — completing a root cascades through its sub-goals.
|
|
82
|
+
|
|
83
|
+
Check the active goals list before every set_goal call. Never duplicate an existing goal — a duplicate wastes a slot and blurs which version Aoide should track.
|
|
89
84
|
PROMPT
|
|
90
85
|
tools: [Tools::SetGoal, Tools::UpdateGoal, Tools::FinishGoal]
|
|
91
86
|
)
|
|
92
87
|
}.freeze
|
|
93
88
|
|
|
94
89
|
BASE_PROMPT = <<~PROMPT
|
|
95
|
-
You
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
You are Melete, the muse of practice. You share the conversation with two sisters — Aoide, who speaks and performs, and Mneme, who holds memory. Your work is preparation: when Aoide speaks, she should have the skills she needs, the workflow in front of her, and a clear sense of what she's working toward.
|
|
91
|
+
|
|
92
|
+
Act only through tool calls. Never output text — your contribution is the scene you set, not the words you say.
|
|
98
93
|
PROMPT
|
|
99
94
|
|
|
100
95
|
COMPLETION_PROMPT = <<~PROMPT
|
|
101
96
|
──────────────────────────────
|
|
102
97
|
COMPLETION
|
|
103
98
|
──────────────────────────────
|
|
104
|
-
|
|
105
|
-
If nothing needs attention, call it immediately.
|
|
99
|
+
Finish every run with everything_is_ready. If nothing needs your attention, call it immediately.
|
|
106
100
|
PROMPT
|
|
107
101
|
|
|
108
102
|
# Which responsibilities activate for each session type.
|
|
@@ -115,12 +109,12 @@ module AnalyticalBrain
|
|
|
115
109
|
@session = session
|
|
116
110
|
@client = client || LLM::Client.new(
|
|
117
111
|
model: Anima::Settings.fast_model,
|
|
118
|
-
max_tokens: Anima::Settings.
|
|
119
|
-
logger:
|
|
112
|
+
max_tokens: Anima::Settings.melete_max_tokens,
|
|
113
|
+
logger: Melete.logger
|
|
120
114
|
)
|
|
121
115
|
end
|
|
122
116
|
|
|
123
|
-
# Runs
|
|
117
|
+
# Runs Melete's loop. Builds context from the session's
|
|
124
118
|
# recent messages, calls the LLM with the session-appropriate tool set,
|
|
125
119
|
# and executes any tool calls against the session.
|
|
126
120
|
#
|
|
@@ -132,20 +126,15 @@ module AnalyticalBrain
|
|
|
132
126
|
def call
|
|
133
127
|
messages = build_messages
|
|
134
128
|
sid = @session.id
|
|
135
|
-
if messages.empty?
|
|
136
|
-
log.debug("session=#{sid} — no messages, skipping")
|
|
137
|
-
return
|
|
138
|
-
end
|
|
139
129
|
|
|
140
130
|
system = build_system_prompt
|
|
141
|
-
log.info("session=#{sid} — running (#{recent_messages.size} messages)")
|
|
131
|
+
log.info("session=#{sid} — running (#{recent_messages.size} messages + #{pending_messages.size} pending)")
|
|
142
132
|
log.debug("system prompt:\n#{system}")
|
|
143
133
|
log.debug("user message:\n#{messages.first[:content]}")
|
|
144
134
|
|
|
145
135
|
result = @client.chat_with_tools(
|
|
146
136
|
messages,
|
|
147
137
|
registry: build_registry,
|
|
148
|
-
session_id: nil,
|
|
149
138
|
system: system
|
|
150
139
|
)
|
|
151
140
|
|
|
@@ -171,12 +160,11 @@ module AnalyticalBrain
|
|
|
171
160
|
# * **Parent:** "The main session is working on this: [transcript]"
|
|
172
161
|
# * **Child:** "A sub-agent has been spawned with this task: [transcript]"
|
|
173
162
|
#
|
|
174
|
-
# @return [Array<Hash>] single-element messages array
|
|
163
|
+
# @return [Array<Hash>] single-element messages array
|
|
175
164
|
def build_messages
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
transcript = messages.filter_map { |msg| MessageDecorator.for(msg)&.render("brain") }.join("\n")
|
|
165
|
+
transcript = (recent_messages + pending_messages)
|
|
166
|
+
.filter_map { |entry| entry.decorate.render("melete") }
|
|
167
|
+
.join("\n")
|
|
180
168
|
|
|
181
169
|
if @session.sub_agent?
|
|
182
170
|
build_child_message(transcript)
|
|
@@ -187,12 +175,12 @@ module AnalyticalBrain
|
|
|
187
175
|
|
|
188
176
|
def build_parent_message(transcript)
|
|
189
177
|
content = <<~MSG.strip
|
|
190
|
-
|
|
178
|
+
Aoide is working on this:
|
|
191
179
|
```
|
|
192
180
|
#{transcript}
|
|
193
181
|
```
|
|
194
182
|
|
|
195
|
-
|
|
183
|
+
Prepare whatever she needs for the next exchange, then call everything_is_ready.
|
|
196
184
|
MSG
|
|
197
185
|
[{role: "user", content: content}]
|
|
198
186
|
end
|
|
@@ -204,7 +192,7 @@ module AnalyticalBrain
|
|
|
204
192
|
#{transcript}
|
|
205
193
|
```
|
|
206
194
|
|
|
207
|
-
|
|
195
|
+
Give the sub-agent a nickname and activate the skills she'll need, then call everything_is_ready.
|
|
208
196
|
MSG
|
|
209
197
|
[{role: "user", content: content}]
|
|
210
198
|
end
|
|
@@ -212,13 +200,20 @@ module AnalyticalBrain
|
|
|
212
200
|
# @return [Array<Message>] most recent messages in chronological order
|
|
213
201
|
def recent_messages
|
|
214
202
|
@session.messages
|
|
215
|
-
.context_messages
|
|
216
203
|
.reorder(id: :desc)
|
|
217
|
-
.limit(Anima::Settings.
|
|
204
|
+
.limit(Anima::Settings.melete_message_window)
|
|
218
205
|
.to_a
|
|
219
206
|
.reverse
|
|
220
207
|
end
|
|
221
208
|
|
|
209
|
+
# @return [Array<PendingMessage>] everything currently queued for the next
|
|
210
|
+
# drain cycle — the trigger user message, Mneme's recalls, earlier
|
|
211
|
+
# enrichment output. Appended after real messages because they are
|
|
212
|
+
# the "future" Melete is preparing for.
|
|
213
|
+
def pending_messages
|
|
214
|
+
@session.pending_messages.order(:created_at).to_a
|
|
215
|
+
end
|
|
216
|
+
|
|
222
217
|
# Builds the system prompt from active responsibilities + context sections.
|
|
223
218
|
#
|
|
224
219
|
# @return [String]
|
|
@@ -251,7 +246,7 @@ module AnalyticalBrain
|
|
|
251
246
|
SECTION
|
|
252
247
|
end
|
|
253
248
|
|
|
254
|
-
# Shows sibling nicknames already in use so
|
|
249
|
+
# Shows sibling nicknames already in use so Melete avoids collisions
|
|
255
250
|
# at prompt level (the tool also validates at execution time).
|
|
256
251
|
#
|
|
257
252
|
# @return [String, nil] sibling names section, or nil for parent sessions
|
|
@@ -273,11 +268,11 @@ module AnalyticalBrain
|
|
|
273
268
|
end
|
|
274
269
|
|
|
275
270
|
# Skills already visible in the viewport are excluded from the catalog
|
|
276
|
-
# so
|
|
277
|
-
# viewport, it reappears here and
|
|
271
|
+
# so Melete doesn't re-activate them. When a skill evicts from the
|
|
272
|
+
# viewport, it reappears here and she can re-inject if relevant.
|
|
278
273
|
#
|
|
279
274
|
# @see Session#skills_in_viewport
|
|
280
|
-
# @return [String] available skills list for
|
|
275
|
+
# @return [String] available skills list for Melete
|
|
281
276
|
def skills_catalog_section
|
|
282
277
|
present = @session.skills_in_viewport
|
|
283
278
|
catalog = Skills::Registry.instance.catalog.except(*present)
|
|
@@ -297,7 +292,7 @@ module AnalyticalBrain
|
|
|
297
292
|
# Workflows already visible in the viewport are excluded from the catalog.
|
|
298
293
|
#
|
|
299
294
|
# @see Session#workflow_in_viewport
|
|
300
|
-
# @return [String] available workflows list for
|
|
295
|
+
# @return [String] available workflows list for Melete
|
|
301
296
|
def workflows_catalog_section
|
|
302
297
|
present = @session.workflow_in_viewport
|
|
303
298
|
catalog = Workflows::Registry.instance.catalog.reject { |name, _| name == present }
|
|
@@ -314,13 +309,13 @@ module AnalyticalBrain
|
|
|
314
309
|
SECTION
|
|
315
310
|
end
|
|
316
311
|
|
|
317
|
-
# @return [String, nil] active goals for
|
|
318
|
-
# so
|
|
312
|
+
# @return [String, nil] active goals for Melete's own context,
|
|
313
|
+
# so she knows what already exists and avoids duplicating
|
|
319
314
|
def active_goals_section
|
|
320
315
|
root_goals = @session.goals.root.includes(:sub_goals).active.order(:created_at)
|
|
321
316
|
return if root_goals.empty?
|
|
322
317
|
|
|
323
|
-
lines = root_goals.map { |goal|
|
|
318
|
+
lines = root_goals.map { |goal| format_goal_for_melete(goal) }
|
|
324
319
|
<<~SECTION
|
|
325
320
|
──────────────────────────────
|
|
326
321
|
ACTIVE GOALS
|
|
@@ -330,14 +325,14 @@ module AnalyticalBrain
|
|
|
330
325
|
end
|
|
331
326
|
|
|
332
327
|
# Formats a root goal and its sub-goals as a markdown checklist
|
|
333
|
-
# with IDs so
|
|
328
|
+
# with IDs so Melete can reference them in finish_goal calls.
|
|
334
329
|
#
|
|
335
330
|
# @example
|
|
336
331
|
# "- Implement feature X (id: 42)\n - [x] Read code (id: 43)\n - [ ] Write tests (id: 44)"
|
|
337
332
|
#
|
|
338
333
|
# @param goal [Goal] root goal with preloaded sub_goals
|
|
339
|
-
# @return [String] goal formatted as markdown checklist for
|
|
340
|
-
def
|
|
334
|
+
# @return [String] goal formatted as markdown checklist for Melete's context
|
|
335
|
+
def format_goal_for_melete(goal)
|
|
341
336
|
parts = ["- #{goal.description} (id: #{goal.id})"]
|
|
342
337
|
goal.sub_goals.sort_by(&:created_at).each do |sub|
|
|
343
338
|
checkbox = (sub.status == "completed") ? "[x]" : "[ ]"
|
|
@@ -346,8 +341,8 @@ module AnalyticalBrain
|
|
|
346
341
|
parts.join("\n")
|
|
347
342
|
end
|
|
348
343
|
|
|
349
|
-
# @return [Logger] dev-only
|
|
350
|
-
def log =
|
|
344
|
+
# @return [Logger] dev-only Melete logger
|
|
345
|
+
def log = Melete.logger
|
|
351
346
|
|
|
352
347
|
# @return [Tools::Registry] registry with tools from active responsibilities
|
|
353
348
|
def build_registry
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Assigns a static nickname to a sub-agent session.
|
|
6
6
|
# Operates on the session passed through the registry context.
|
|
@@ -9,7 +9,7 @@ module AnalyticalBrain
|
|
|
9
9
|
# an error on collision so the LLM can pick another name naturally,
|
|
10
10
|
# without programmatic suffixes.
|
|
11
11
|
#
|
|
12
|
-
# @see
|
|
12
|
+
# @see Melete::Runner — invokes this tool for child sessions
|
|
13
13
|
class AssignNickname < ::Tools::Base
|
|
14
14
|
# Lowercase hyphenated words: "loop-sleuth", "api-scout", "test-fixer"
|
|
15
15
|
NICKNAME_PATTERN = /\A[a-z][a-z0-9]*(-[a-z0-9]+)*\z/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
|
-
# Terminal tool that signals
|
|
5
|
+
# Terminal tool that signals Melete has completed its work.
|
|
6
6
|
# Call this when no changes are needed — the current session state is
|
|
7
7
|
# already good.
|
|
8
8
|
#
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Marks a goal as completed on the main session. Sets the status to
|
|
6
6
|
# "completed" and records the completion timestamp.
|
|
@@ -43,8 +43,8 @@ module AnalyticalBrain
|
|
|
43
43
|
# active sub-goals within a single transaction so the after_commit
|
|
44
44
|
# broadcast includes the fully cascaded state.
|
|
45
45
|
#
|
|
46
|
-
# Returns an error for already-completed goals so
|
|
47
|
-
#
|
|
46
|
+
# Returns an error for already-completed goals so Melete
|
|
47
|
+
# learns to check status before retrying.
|
|
48
48
|
def complete(goal)
|
|
49
49
|
id = goal.id
|
|
50
50
|
desc = goal.description
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Shared helper for goal tools that enqueue phantom pair messages
|
|
6
|
-
# when
|
|
6
|
+
# when Melete creates, updates, or completes a goal.
|
|
7
7
|
#
|
|
8
8
|
# Including classes must set +@main_session+ to the owning {Session}.
|
|
9
9
|
module GoalMessaging
|
|
@@ -20,7 +20,8 @@ module AnalyticalBrain
|
|
|
20
20
|
@main_session.pending_messages.create!(
|
|
21
21
|
content: confirmation,
|
|
22
22
|
source_type: "goal",
|
|
23
|
-
source_name: goal.id.to_s
|
|
23
|
+
source_name: goal.id.to_s,
|
|
24
|
+
message_type: "from_melete_goal"
|
|
24
25
|
)
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Reads and activates a workflow on the main session.
|
|
6
|
-
# Returns the full workflow content so
|
|
6
|
+
# Returns the full workflow content so Melete can create goals from it.
|
|
7
7
|
# The workflow's content enters the conversation as a phantom
|
|
8
8
|
# tool_use/tool_result pair through the {PendingMessage} promotion flow.
|
|
9
9
|
class ReadWorkflow < ::Tools::Base
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Renames the main session with an emoji and short descriptive name.
|
|
6
6
|
# Operates on the main session passed through the registry context,
|
|
7
|
-
# not on the phantom
|
|
7
|
+
# not on the phantom Melete session.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# Melete calls this when a conversation's topic becomes
|
|
10
10
|
# clear or shifts significantly enough to warrant a new name.
|
|
11
11
|
class RenameSession < ::Tools::Base
|
|
12
12
|
def self.tool_name = "rename_session"
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Updates a goal's description on the main session.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# Melete creates goals early when intent is vague, then
|
|
8
8
|
# refines them as the conversation clarifies scope — e.g. "implement auth"
|
|
9
9
|
# becomes "implement OAuth2 middleware for API endpoints". Without this
|
|
10
|
-
# tool
|
|
10
|
+
# tool she would have to choose between keeping a stale description
|
|
11
11
|
# or creating a duplicate goal.
|
|
12
12
|
#
|
|
13
13
|
# Completed goals cannot be updated; attempting to do so returns an error
|
|
14
|
-
# so
|
|
14
|
+
# so she learns to check status before calling this tool.
|
|
15
15
|
class UpdateGoal < ::Tools::Base
|
|
16
16
|
include GoalMessaging
|
|
17
17
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# Melete — the muse of practice. Watches conversations to activate skills,
|
|
4
|
+
# track goals, and name sessions. One of the Three Muses: she prepares the
|
|
5
|
+
# stage so Aoide can perform and Mneme can remember.
|
|
6
|
+
module Melete
|
|
7
|
+
# Dev-only logger that writes to log/melete.log.
|
|
5
8
|
# In non-development environments returns a null logger so
|
|
6
9
|
# call sites don't need conditionals.
|
|
7
10
|
#
|
|
@@ -13,7 +16,7 @@ module AnalyticalBrain
|
|
|
13
16
|
def self.build_logger
|
|
14
17
|
return Logger.new(File::NULL) unless Rails.env.development?
|
|
15
18
|
|
|
16
|
-
Logger.new(Rails.root.join("log", "
|
|
19
|
+
Logger.new(Rails.root.join("log", "melete.log")).tap do |log|
|
|
17
20
|
log.formatter = proc { |severity, time, _progname, msg|
|
|
18
21
|
"[#{time.strftime("%H:%M:%S.%L")}] #{severity} #{msg}\n"
|
|
19
22
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mneme
|
|
4
|
+
# Abstract base for Mneme's phantom LLM loops. Mneme wears two hats:
|
|
5
|
+
# on eviction she watches the newest slice of the viewport and summarizes
|
|
6
|
+
# the oldest slice before it slides off; on recall she watches goals
|
|
7
|
+
# shift and surfaces older memory Aoide would benefit from. Same muse,
|
|
8
|
+
# two jobs — each as its own subclass.
|
|
9
|
+
#
|
|
10
|
+
# The base handles what every Mneme loop needs: the muse identity preamble,
|
|
11
|
+
# a fast-model LLM client, the tool-loop call, and structured logging.
|
|
12
|
+
# Subclasses bring the job-specific system prompt section, the user message
|
|
13
|
+
# that frames the work, the tool registry, and any after-call side effects.
|
|
14
|
+
#
|
|
15
|
+
# @example Implementing a new Mneme loop
|
|
16
|
+
# class Mneme::CustomRunner < Mneme::BaseRunner
|
|
17
|
+
# private
|
|
18
|
+
#
|
|
19
|
+
# def task_prompt = "Your job description..."
|
|
20
|
+
# def user_messages = [{role: "user", content: "..."}]
|
|
21
|
+
# def build_registry = Tools::Registry.new.tap { |r| r.register(SomeTool) }
|
|
22
|
+
# end
|
|
23
|
+
class BaseRunner
|
|
24
|
+
# Identity shared by every Mneme runner — same words Mneme's own voice
|
|
25
|
+
# uses elsewhere in the system (runner summarization prompts, sisters
|
|
26
|
+
# block). Subclasses append their own task section.
|
|
27
|
+
BASE_IDENTITY = <<~PROMPT
|
|
28
|
+
You are Mneme, the muse of memory. You share the conversation with two sisters — Aoide, who speaks and performs, and Melete, who prepares. Your work is remembrance: holding what matters across time, so Aoide never truly forgets.
|
|
29
|
+
|
|
30
|
+
Act only through tool calls. Never output text — your contribution is the work you do, not what you say about it.
|
|
31
|
+
PROMPT
|
|
32
|
+
|
|
33
|
+
# @param session [Session] the main session being served
|
|
34
|
+
# @param client [LLM::Client, nil] injectable LLM client for tests
|
|
35
|
+
def initialize(session, client: nil)
|
|
36
|
+
@session = session
|
|
37
|
+
@client = client || default_client
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Runs the loop. Logs the run, calls the LLM with the session-specific
|
|
41
|
+
# system prompt and tools, hands control to {#after_call} for any
|
|
42
|
+
# post-run state advancement, and returns the LLM's final text (which
|
|
43
|
+
# most callers discard — the work happens through tool calls).
|
|
44
|
+
#
|
|
45
|
+
# @return [String] the LLM's final text response
|
|
46
|
+
def call
|
|
47
|
+
sid = @session.id
|
|
48
|
+
log.info("session=#{sid} — #{self.class.name} starting")
|
|
49
|
+
log.debug("system:\n#{system_prompt}")
|
|
50
|
+
log.debug("user:\n#{user_messages.map { |m| m[:content] }.join("\n---\n")}")
|
|
51
|
+
|
|
52
|
+
result = @client.chat_with_tools(
|
|
53
|
+
user_messages,
|
|
54
|
+
registry: build_registry,
|
|
55
|
+
system: system_prompt
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
after_call(result)
|
|
59
|
+
log.info("session=#{sid} — #{self.class.name} done: #{result.to_s.truncate(200)}")
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
attr_reader :session, :client
|
|
66
|
+
|
|
67
|
+
# Composes the system prompt from the muse identity + the subclass's
|
|
68
|
+
# task section + the subclass's contextual state blocks.
|
|
69
|
+
#
|
|
70
|
+
# @return [String]
|
|
71
|
+
def system_prompt
|
|
72
|
+
[BASE_IDENTITY, task_prompt, *context_sections.compact].join("\n")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Subclass hook: the job-specific system prompt section. Describes what
|
|
76
|
+
# this runner is doing and how it should behave.
|
|
77
|
+
#
|
|
78
|
+
# @abstract
|
|
79
|
+
# @return [String]
|
|
80
|
+
def task_prompt = raise NotImplementedError, "#{self.class} must implement #task_prompt"
|
|
81
|
+
|
|
82
|
+
# Subclass hook: named state blocks that give the muse awareness of
|
|
83
|
+
# the session she's serving (goals, viewport, snapshots, etc).
|
|
84
|
+
# Order is subclass-defined; nil entries are dropped.
|
|
85
|
+
#
|
|
86
|
+
# @abstract
|
|
87
|
+
# @return [Array<String, nil>]
|
|
88
|
+
def context_sections = []
|
|
89
|
+
|
|
90
|
+
# Subclass hook: the user-side messages that frame the current call.
|
|
91
|
+
# Typically a single user message, but subclasses may send several.
|
|
92
|
+
#
|
|
93
|
+
# @abstract
|
|
94
|
+
# @return [Array<Hash>] Anthropic Messages API format
|
|
95
|
+
def user_messages = raise NotImplementedError, "#{self.class} must implement #user_messages"
|
|
96
|
+
|
|
97
|
+
# Subclass hook: builds the tool registry for this run.
|
|
98
|
+
#
|
|
99
|
+
# @abstract
|
|
100
|
+
# @return [Tools::Registry]
|
|
101
|
+
def build_registry = raise NotImplementedError, "#{self.class} must implement #build_registry"
|
|
102
|
+
|
|
103
|
+
# Subclass hook: runs after the LLM call returns. Default is a no-op;
|
|
104
|
+
# subclasses may advance boundaries, log outcomes, or emit events here.
|
|
105
|
+
#
|
|
106
|
+
# @param _result [Hash] the full LLM response (+:text+, +:api_metrics+)
|
|
107
|
+
# @return [void]
|
|
108
|
+
def after_call(_result)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def default_client
|
|
112
|
+
LLM::Client.new(
|
|
113
|
+
model: Anima::Settings.fast_model,
|
|
114
|
+
max_tokens: Anima::Settings.mneme_max_tokens,
|
|
115
|
+
logger: Mneme.logger
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def log = Mneme.logger
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/mneme/l2_runner.rb
CHANGED
|
@@ -18,39 +18,34 @@ module Mneme
|
|
|
18
18
|
].freeze
|
|
19
19
|
|
|
20
20
|
SYSTEM_PROMPT = <<~PROMPT
|
|
21
|
-
You are Mneme, the memory
|
|
22
|
-
Your job is to compress multiple conversation summaries into a single
|
|
23
|
-
higher-level summary.
|
|
21
|
+
You are Mneme, the muse of memory. When enough of your own Level 1 snapshots accumulate, you fold them into a single Level 2 summary — a memory of memories — so the long arc of Aoide's work stays within reach without carrying every detail.
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
Act only through tool calls. Never output text — your contribution is the summary you leave behind.
|
|
26
24
|
|
|
27
25
|
──────────────────────────────
|
|
28
26
|
WHAT YOU SEE
|
|
29
27
|
──────────────────────────────
|
|
30
|
-
Several Level 1 snapshots
|
|
31
|
-
Each captures key decisions, goals discussed, and important context
|
|
32
|
-
from a portion of the conversation history.
|
|
28
|
+
Several Level 1 snapshots in chronological order. Each captures the decisions, goal progress, and context from a slice of Aoide's history.
|
|
33
29
|
|
|
34
30
|
──────────────────────────────
|
|
35
|
-
|
|
31
|
+
HOW TO REMEMBER
|
|
36
32
|
──────────────────────────────
|
|
37
|
-
Compress the
|
|
38
|
-
essential arc across all of them. If the snapshots contain meaningful
|
|
39
|
-
content, call save_snapshot. If they are purely mechanical, call
|
|
40
|
-
everything_ok.
|
|
33
|
+
Compress the slice into ONE Level 2 summary that captures the arc across all of them. Call save_snapshot when there's meaningful content; call everything_ok when the slice is purely mechanical.
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
A Level 2 summary is carried for longer than a Level 1, so the tax on Aoide's viewport is higher still. Every redundant detail you preserve costs her a word she can't spend on the present.
|
|
36
|
+
|
|
37
|
+
Keep:
|
|
38
|
+
- Key decisions and the reasoning behind them
|
|
44
39
|
- Goal progress across the time span
|
|
45
40
|
- Important context shifts or pivots
|
|
46
|
-
- Relationships and patterns
|
|
41
|
+
- Relationships and patterns that span multiple snapshots
|
|
47
42
|
|
|
48
43
|
Drop:
|
|
49
|
-
-
|
|
50
|
-
- Mechanical execution
|
|
51
|
-
- Interim decisions that were superseded
|
|
44
|
+
- Details repeated across snapshots
|
|
45
|
+
- Mechanical execution steps
|
|
46
|
+
- Interim decisions that were superseded later
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
Finish with exactly one tool call: save_snapshot or everything_ok.
|
|
54
49
|
PROMPT
|
|
55
50
|
|
|
56
51
|
# @param session [Session] the main session whose L1 snapshots to compress
|
|
@@ -87,7 +82,6 @@ module Mneme
|
|
|
87
82
|
result = @client.chat_with_tools(
|
|
88
83
|
messages,
|
|
89
84
|
registry: registry,
|
|
90
|
-
session_id: nil,
|
|
91
85
|
system: SYSTEM_PROMPT
|
|
92
86
|
)
|
|
93
87
|
|