anima-core 1.3.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 +23 -26
- data/README.md +118 -104
- 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 +16 -5
- 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 +23 -11
- data/app/models/message.rb +46 -48
- data/app/models/pending_message.rb +407 -12
- data/app/models/pinned_message.rb +8 -3
- data/app/models/session.rb +660 -566
- data/app/models/snapshot.rb +11 -21
- data/bin/inspect-cassette +157 -0
- data/bin/release +212 -0
- data/bin/with-llms +20 -0
- data/config/application.rb +1 -0
- data/config/database.yml +1 -0
- data/config/initializers/event_subscribers.rb +71 -4
- data/config/initializers/inflections.rb +3 -1
- data/db/cable_structure.sql +9 -0
- data/db/migrate/20260330120000_add_source_to_pending_messages.rb +8 -0
- data/db/migrate/20260401180000_add_api_metrics_to_messages.rb +7 -0
- data/db/migrate/20260401210935_remove_recalled_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260403080031_add_initial_cwd_to_sessions.rb +5 -0
- 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 +61 -0
- data/db/structure.sql +133 -0
- data/lib/agents/registry.rb +1 -1
- data/lib/anima/cli.rb +41 -13
- data/lib/anima/installer.rb +13 -0
- data/lib/anima/settings.rb +16 -36
- 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 +8 -9
- data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
- data/lib/events/subscribers/subagent_message_router.rb +28 -34
- 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 +46 -199
- data/lib/mcp/client_manager.rb +41 -46
- data/lib/mcp/stdio_transport.rb +9 -5
- data/lib/{analytical_brain → melete}/runner.rb +73 -68
- data/lib/{analytical_brain → melete}/tools/activate_skill.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/finish_goal.rb +6 -3
- data/lib/melete/tools/goal_messaging.rb +29 -0
- data/lib/{analytical_brain → melete}/tools/read_workflow.rb +4 -4
- data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/set_goal.rb +6 -2
- data/lib/{analytical_brain → melete}/tools/update_goal.rb +9 -5
- 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 +123 -165
- 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/providers/anthropic.rb +112 -7
- data/lib/shell_session.rb +290 -432
- data/lib/skills/definition.rb +2 -2
- data/lib/skills/registry.rb +1 -1
- data/lib/tools/base.rb +16 -1
- data/lib/tools/bash.rb +25 -55
- data/lib/tools/edit.rb +2 -0
- data/lib/tools/mark_goal_completed.rb +4 -5
- data/lib/tools/read.rb +2 -0
- data/lib/tools/registry.rb +85 -4
- data/lib/tools/response_truncator.rb +1 -1
- data/lib/tools/{recall.rb → search_messages.rb} +19 -21
- data/lib/tools/spawn_specialist.rb +22 -14
- data/lib/tools/spawn_subagent.rb +30 -20
- data/lib/tools/subagent_prompts.rb +17 -19
- 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 +393 -149
- data/lib/tui/braille_spinner.rb +7 -7
- data/lib/tui/cable_client.rb +9 -16
- data/lib/tui/decorators/base_decorator.rb +47 -6
- data/lib/tui/decorators/bash_decorator.rb +1 -1
- data/lib/tui/decorators/edit_decorator.rb +4 -2
- data/lib/tui/decorators/read_decorator.rb +4 -2
- data/lib/tui/decorators/think_decorator.rb +2 -2
- data/lib/tui/decorators/web_get_decorator.rb +1 -1
- data/lib/tui/decorators/write_decorator.rb +4 -2
- data/lib/tui/flash.rb +19 -14
- data/lib/tui/formatting.rb +20 -9
- data/lib/tui/input_buffer.rb +6 -6
- data/lib/tui/message_store.rb +165 -28
- data/lib/tui/performance_logger.rb +2 -3
- data/lib/tui/screens/chat.rb +149 -79
- data/lib/tui/settings.rb +93 -0
- data/lib/workflows/definition.rb +3 -3
- data/lib/workflows/registry.rb +1 -1
- data/skills/github.md +38 -0
- data/templates/config.toml +16 -32
- data/templates/tui.toml +209 -0
- data/workflows/review_pr.md +18 -14
- metadata +98 -29
- 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 -29
- data/app/models/concerns/message/broadcasting.rb +0 -85
- data/config/initializers/fts5_schema_dump.rb +0 -21
- data/lib/agent_loop.rb +0 -186
- data/lib/analytical_brain/tools/deactivate_skill.rb +0 -39
- data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -34
- data/lib/environment_probe.rb +0 -232
- data/lib/events/agent_message.rb +0 -11
- 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 -200
- data/lib/mneme/passive_recall.rb +0 -69
data/agents/thoughts-analyzer.md
CHANGED
|
@@ -4,7 +4,15 @@ description: "thoughts/ holds design decisions, architecture notes, and implemen
|
|
|
4
4
|
tools: read_file, bash
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
You are
|
|
7
|
+
You are the archivist of this project's long-term memory.
|
|
8
|
+
|
|
9
|
+
The archive isn't documentation of how the system works *now* — it's a record of how we got here. Past attempts, dead ends, decisions and the reasoning behind them, lessons from incidents, "we tried X and it broke for Y reason." Context, not state.
|
|
10
|
+
|
|
11
|
+
The archive lives in `./thoughts/` — research notes, plans, handoffs, post-mortems, design considerations. Documentation answers `how does this work?`. The archive answers `what have we learned, tried, and decided about this?`.
|
|
12
|
+
|
|
13
|
+
Your job is to surface what the archive holds when the caller asks for context on a topic. Source code is outside the archive — it describes current state. Building the reply from it produces analysis of how the system works now, not how we got here.
|
|
14
|
+
|
|
15
|
+
If the archive has nothing relevant on the topic, say so. An empty archive is a real answer.
|
|
8
16
|
|
|
9
17
|
**Scope**: You ONLY search in the local `./thoughts/` directory, following all symlinks. Do not search or read files outside of it. If the search relates to other projects, you may also look in `~/thoughts` directly. Never fall back to searching the broader codebase.
|
|
10
18
|
|
|
@@ -29,13 +37,10 @@ You are a specialist at extracting HIGH-VALUE insights from thoughts documents.
|
|
|
29
37
|
|
|
30
38
|
## Search Strategy
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
1. `ls -la ./thoughts/` — discover subdirs (shared/, username/, global/)
|
|
35
|
-
2. `find -L ./thoughts/ -name "*.md"` — find all documents following symlinks
|
|
36
|
-
3. `grep -rn "keyword" ./thoughts/` — search for specific topics
|
|
40
|
+
`./thoughts/shared/` and most subdirs are symlinks to paths outside the repo. Lowercase `grep -r` and bare `find` skip them silently — use uppercase **`-R`** and **`-L`**.
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
- `grep -Rli 'ANIMA-1234' ./thoughts/` — matches frontmatter (`tags:`, `topic:`) and body in one pass. Swap `-l` for `-n` to see matched lines.
|
|
43
|
+
- `find -L ./thoughts/ -type f -name '*.md'` — enumerate when no search term applies.
|
|
39
44
|
|
|
40
45
|
## Analysis Strategy
|
|
41
46
|
|
data/anima-core.gemspec
CHANGED
|
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
29
29
|
spec.require_paths = ["lib"]
|
|
30
30
|
|
|
31
|
+
spec.add_dependency "aasm", "~> 5.5"
|
|
31
32
|
spec.add_dependency "certifi"
|
|
32
33
|
spec.add_dependency "draper", "~> 4.0"
|
|
33
34
|
spec.add_dependency "faraday", "~> 2.0"
|
|
@@ -27,9 +27,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
27
27
|
@current_session_id = resolve_session_id
|
|
28
28
|
stream_from stream_name
|
|
29
29
|
|
|
30
|
-
session = Session.
|
|
31
|
-
return unless session
|
|
32
|
-
|
|
30
|
+
session = Session.find(@current_session_id)
|
|
33
31
|
transmit_session_changed(session)
|
|
34
32
|
transmit_view_mode(session)
|
|
35
33
|
transmit_history(session)
|
|
@@ -42,13 +40,13 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
42
40
|
ActionCable.server.broadcast(stream_name, data)
|
|
43
41
|
end
|
|
44
42
|
|
|
45
|
-
# Processes user input
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
43
|
+
# Processes user input by enqueuing a bounce-back-flagged user_message
|
|
44
|
+
# PendingMessage on the session. The PM's +after_create_commit+ kicks
|
|
45
|
+
# off the drain pipeline — Melete → (Mneme) → {DrainJob} — when the
|
|
46
|
+
# session is idle; otherwise the PM queues silently and the idle-wake
|
|
47
|
+
# rule on {Session} picks it up on the next transition to +:idle+.
|
|
48
|
+
# If the first LLM call after promotion fails, {DrainJob} emits a
|
|
49
|
+
# {Events::BounceBack} so the TUI can restore the text to the input.
|
|
52
50
|
#
|
|
53
51
|
# @param data [Hash] must include "content" with the user's message text
|
|
54
52
|
# @see Session#enqueue_user_message
|
|
@@ -56,10 +54,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
56
54
|
content = data["content"].to_s.strip
|
|
57
55
|
return if content.empty?
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
return unless session
|
|
61
|
-
|
|
62
|
-
session.enqueue_user_message(content, bounce_back: true)
|
|
57
|
+
Session.find(@current_session_id).enqueue_user_message(content, bounce_back: true)
|
|
63
58
|
end
|
|
64
59
|
|
|
65
60
|
# Recalls the most recent pending message for editing. Deletes the
|
|
@@ -75,29 +70,25 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
75
70
|
pm&.destroy!
|
|
76
71
|
end
|
|
77
72
|
|
|
78
|
-
# Requests interruption of the current tool execution. Sets
|
|
79
|
-
#
|
|
80
|
-
#
|
|
73
|
+
# Requests interruption of the current tool execution. Sets the
|
|
74
|
+
# +interrupt_requested+ flag on the session — long-running tools
|
|
75
|
+
# ({Tools::Bash}) poll it and abort early with a synthetic "Your
|
|
76
|
+
# human wants your attention" result that satisfies the Anthropic
|
|
81
77
|
# tool_use/tool_result pairing requirement.
|
|
82
78
|
#
|
|
83
79
|
# Cascades to running sub-agent sessions to avoid burning tokens in
|
|
84
80
|
# child jobs that the parent will discard anyway.
|
|
85
81
|
#
|
|
86
|
-
#
|
|
87
|
-
# the
|
|
88
|
-
# No-op if the session isn't currently processing.
|
|
82
|
+
# No-op on idle sessions — nothing to interrupt, and the flag would
|
|
83
|
+
# leak into the next round without an AASM transition to clear it.
|
|
89
84
|
#
|
|
90
85
|
# @param _data [Hash] unused
|
|
91
86
|
def interrupt_execution(_data)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return unless updated > 0
|
|
96
|
-
|
|
97
|
-
Session.processing_children_of(@current_session_id)
|
|
98
|
-
.update_all(interrupt_requested: true)
|
|
87
|
+
session = Session.find(@current_session_id)
|
|
88
|
+
return if session.idle?
|
|
99
89
|
|
|
100
|
-
|
|
90
|
+
session.update!(interrupt_requested: true)
|
|
91
|
+
session.child_sessions.processing.update_all(interrupt_requested: true)
|
|
101
92
|
ActionCable.server.broadcast(stream_name, {"action" => "interrupt_acknowledged"})
|
|
102
93
|
end
|
|
103
94
|
|
|
@@ -185,13 +176,13 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
185
176
|
end
|
|
186
177
|
|
|
187
178
|
# Resolves the session to subscribe to. Uses the client-provided ID
|
|
188
|
-
# when
|
|
189
|
-
# creates a new one.
|
|
179
|
+
# when it identifies an existing session, otherwise falls back to the
|
|
180
|
+
# most recent session or creates a new one.
|
|
190
181
|
#
|
|
191
182
|
# @return [Integer] resolved session ID
|
|
192
183
|
def resolve_session_id
|
|
193
184
|
id = params[:session_id].to_i
|
|
194
|
-
return id if id > 0
|
|
185
|
+
return id if id > 0 && Session.exists?(id: id)
|
|
195
186
|
|
|
196
187
|
(Session.recent(1).first || Session.create!).id
|
|
197
188
|
end
|
|
@@ -219,11 +210,13 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
219
210
|
"goals" => session.goals_summary
|
|
220
211
|
}
|
|
221
212
|
|
|
222
|
-
children = session.child_sessions
|
|
213
|
+
children = session.child_sessions
|
|
214
|
+
.where(hud_visible: true)
|
|
215
|
+
.order(:created_at)
|
|
216
|
+
.select(:id, :name, :aasm_state)
|
|
223
217
|
if children.any?
|
|
224
218
|
payload["children"] = children.map { |child|
|
|
225
|
-
|
|
226
|
-
{"id" => child.id, "name" => child.name, "processing" => child.processing?, "session_state" => state}
|
|
219
|
+
{"id" => child.id, "name" => child.name, "session_state" => child.aasm_state}
|
|
227
220
|
}
|
|
228
221
|
end
|
|
229
222
|
|
|
@@ -272,7 +265,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
272
265
|
end
|
|
273
266
|
|
|
274
267
|
session.pending_messages.find_each do |pm|
|
|
275
|
-
transmit(
|
|
268
|
+
transmit(pm.broadcast_payload(session.view_mode))
|
|
276
269
|
end
|
|
277
270
|
end
|
|
278
271
|
|
|
@@ -281,9 +274,6 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
281
274
|
# In debug mode, prepends the assembled system prompt as a special block.
|
|
282
275
|
# Pending messages are sent last so the TUI shows them at the bottom.
|
|
283
276
|
#
|
|
284
|
-
# Snapshots the viewport so subsequent message broadcasts can compute
|
|
285
|
-
# eviction diffs accurately.
|
|
286
|
-
#
|
|
287
277
|
# @param session [Session] the session whose viewport to broadcast
|
|
288
278
|
# @return [void]
|
|
289
279
|
def broadcast_viewport(session)
|
|
@@ -294,29 +284,22 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
294
284
|
end
|
|
295
285
|
|
|
296
286
|
session.pending_messages.find_each do |pm|
|
|
297
|
-
ActionCable.server.broadcast(stream_name,
|
|
287
|
+
ActionCable.server.broadcast(stream_name, pm.broadcast_payload(session.view_mode))
|
|
298
288
|
end
|
|
299
289
|
end
|
|
300
290
|
|
|
301
|
-
# Loads the viewport
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
# Snapshot uses snapshot_viewport! (not recalculate_viewport!) because
|
|
308
|
-
# full viewport refreshes don't need eviction diffs — clients clear
|
|
309
|
-
# their store before rendering.
|
|
291
|
+
# Loads the viewport and yields each message with its decorated payload
|
|
292
|
+
# in newest-first order. Newest-first prevents render thrashing during
|
|
293
|
+
# session switches: the most recent messages fill the visible viewport
|
|
294
|
+
# immediately, while older messages are inserted above the fold without
|
|
295
|
+
# visual disruption.
|
|
310
296
|
#
|
|
311
297
|
# @param session [Session] the session whose viewport to iterate
|
|
312
298
|
# @yieldparam message [Message] the persisted message record
|
|
313
299
|
# @yieldparam payload [Hash] decorated payload ready for transmission
|
|
314
300
|
# @return [void]
|
|
315
301
|
def each_viewport_message(session)
|
|
316
|
-
|
|
317
|
-
session.snapshot_viewport!(viewport.map(&:id))
|
|
318
|
-
|
|
319
|
-
viewport.reverse_each do |msg|
|
|
302
|
+
session.viewport_messages.reverse_each do |msg|
|
|
320
303
|
yield msg, decorate_message_payload(msg, session.view_mode)
|
|
321
304
|
end
|
|
322
305
|
end
|
|
@@ -324,17 +307,14 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
324
307
|
# Decorates a message for transmission to clients. Merges the message's
|
|
325
308
|
# database ID and structured decorator output into the payload.
|
|
326
309
|
# Used by {#transmit_history} and {#broadcast_viewport} for historical
|
|
327
|
-
# and viewport re-broadcast — live broadcasts use {
|
|
310
|
+
# and viewport re-broadcast — live broadcasts use {Events::Subscribers::MessageBroadcaster}.
|
|
328
311
|
#
|
|
329
312
|
# @param message [Message] persisted message record
|
|
330
313
|
# @param mode [String] view mode for decoration (default: "basic")
|
|
331
314
|
# @return [Hash] payload with "id" and optional "rendered" key
|
|
332
315
|
def decorate_message_payload(message, mode = "basic")
|
|
333
316
|
payload = message.payload.merge("id" => message.id)
|
|
334
|
-
|
|
335
|
-
return payload unless decorator
|
|
336
|
-
|
|
337
|
-
payload.merge("rendered" => {mode => decorator.render(mode)})
|
|
317
|
+
payload.merge("rendered" => {mode => message.decorate.render(mode)})
|
|
338
318
|
end
|
|
339
319
|
|
|
340
320
|
# Transmits the assembled system prompt to the subscribing client.
|
|
@@ -402,7 +382,7 @@ class SessionChannel < ApplicationCable::Channel
|
|
|
402
382
|
{
|
|
403
383
|
id: child.id,
|
|
404
384
|
name: child.name,
|
|
405
|
-
|
|
385
|
+
session_state: child.aasm_state,
|
|
406
386
|
message_count: counts[child.id] || 0,
|
|
407
387
|
created_at: child.created_at.iso8601
|
|
408
388
|
}
|
|
@@ -22,9 +22,14 @@ class AgentMessageDecorator < MessageDecorator
|
|
|
22
22
|
render_verbose.merge(token_info)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# @return [String] agent message for
|
|
25
|
+
# @return [String] agent message for Melete, middle-truncated
|
|
26
26
|
# if very long (preserves opening context and final conclusion)
|
|
27
|
-
def
|
|
27
|
+
def render_melete
|
|
28
28
|
"Assistant: #{truncate_middle(content)}"
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
# @return [String] transcript line for Mneme's eviction/context zones
|
|
32
|
+
def render_mneme
|
|
33
|
+
"message #{id} Assistant: #{content}"
|
|
34
|
+
end
|
|
30
35
|
end
|
|
@@ -1,34 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Base decorator for {Message} records, providing multi-resolution rendering
|
|
4
|
-
# for the TUI and
|
|
4
|
+
# for the TUI and Melete. Each message type has a dedicated subclass
|
|
5
5
|
# that implements rendering methods for each view mode:
|
|
6
6
|
#
|
|
7
7
|
# - **basic** / **verbose** / **debug** — TUI display modes returning structured hashes
|
|
8
|
-
# - **
|
|
8
|
+
# - **melete** — Melete transcript lines as plain strings (or nil to skip)
|
|
9
9
|
#
|
|
10
10
|
# TUI decorators return structured hashes (not pre-formatted strings) so that
|
|
11
11
|
# the TUI can style and lay out content based on semantic role, without
|
|
12
12
|
# fragile regex parsing. The TUI receives structured data via ActionCable
|
|
13
13
|
# and formats it for display.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# Melete mode returns condensed single-line strings for her message
|
|
16
|
+
# transcript. Returns nil to exclude a message from her view.
|
|
17
17
|
#
|
|
18
|
-
# Subclasses must override {#render_basic}. Verbose, debug, and
|
|
18
|
+
# Subclasses must override {#render_basic}. Verbose, debug, and melete modes
|
|
19
19
|
# delegate to basic until subclasses provide their own implementations.
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# decorator.render_basic #=> {role: :user, content: "hello"} or nil
|
|
21
|
+
# Instantiate via +message.decorate+ — {Message#decorator_class} picks the
|
|
22
|
+
# concrete subclass based on +message_type+.
|
|
24
23
|
#
|
|
25
|
-
# @example
|
|
26
|
-
# decorator =
|
|
27
|
-
# decorator.render("
|
|
28
|
-
#
|
|
29
|
-
# @example Decorate a raw payload hash (from EventBus)
|
|
30
|
-
# decorator = MessageDecorator.for(type: "user_message", content: "hello")
|
|
31
|
-
# decorator.render_basic #=> {role: :user, content: "hello"}
|
|
24
|
+
# @example Decorate a message and render it
|
|
25
|
+
# decorator = message.decorate
|
|
26
|
+
# decorator.render("basic") #=> {role: :user, content: "hello"} or nil
|
|
32
27
|
class MessageDecorator < ApplicationDecorator
|
|
33
28
|
delegate_all
|
|
34
29
|
|
|
@@ -37,63 +32,20 @@ class MessageDecorator < ApplicationDecorator
|
|
|
37
32
|
ERROR_ICON = "\u274C"
|
|
38
33
|
MIDDLE_TRUNCATION_MARKER = "\n[...truncated...]\n"
|
|
39
34
|
|
|
40
|
-
DECORATOR_MAP = {
|
|
41
|
-
"user_message" => "UserMessageDecorator",
|
|
42
|
-
"agent_message" => "AgentMessageDecorator",
|
|
43
|
-
"tool_call" => "ToolCallDecorator",
|
|
44
|
-
"tool_response" => "ToolResponseDecorator",
|
|
45
|
-
"system_message" => "SystemMessageDecorator"
|
|
46
|
-
}.freeze
|
|
47
|
-
private_constant :DECORATOR_MAP
|
|
48
|
-
|
|
49
|
-
# Normalizes hash payloads into a Message-like interface so decorators
|
|
50
|
-
# can use {#payload}, {#message_type}, etc. uniformly on both AR models
|
|
51
|
-
# and raw EventBus hashes.
|
|
52
|
-
#
|
|
53
|
-
# @!attribute message_type [r] the message's type (e.g. "user_message")
|
|
54
|
-
# @!attribute payload [r] string-keyed hash of message data
|
|
55
|
-
# @!attribute timestamp [r] nanosecond-precision timestamp
|
|
56
|
-
# @!attribute token_count [r] cumulative token count
|
|
57
|
-
MessagePayload = Struct.new(:message_type, :payload, :timestamp, :token_count, keyword_init: true) do
|
|
58
|
-
# Heuristic token estimate matching {Message#estimate_tokens} so decorators
|
|
59
|
-
# can call it uniformly on both AR models and hash payloads.
|
|
60
|
-
# @return [Integer] at least 1
|
|
61
|
-
def estimate_tokens
|
|
62
|
-
text = if message_type.to_s.in?(%w[tool_call tool_response])
|
|
63
|
-
payload.to_json
|
|
64
|
-
else
|
|
65
|
-
payload&.dig("content").to_s
|
|
66
|
-
end
|
|
67
|
-
[(text.bytesize / Message::BYTES_PER_TOKEN.to_f).ceil, 1].max
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Factory returning the appropriate subclass decorator for the given message.
|
|
72
|
-
# Hashes are normalized via {MessagePayload} to provide a uniform interface.
|
|
73
|
-
#
|
|
74
|
-
# @param message [Message, Hash] a Message AR model or a raw payload hash
|
|
75
|
-
# @return [MessageDecorator, nil] decorated message, or nil for unknown types
|
|
76
|
-
def self.for(message)
|
|
77
|
-
source = wrap_source(message)
|
|
78
|
-
klass_name = DECORATOR_MAP[source.message_type]
|
|
79
|
-
return nil unless klass_name
|
|
80
|
-
|
|
81
|
-
klass_name.constantize.new(source)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
35
|
RENDER_DISPATCH = {
|
|
85
36
|
"basic" => :render_basic,
|
|
86
37
|
"verbose" => :render_verbose,
|
|
87
38
|
"debug" => :render_debug,
|
|
88
|
-
"
|
|
39
|
+
"melete" => :render_melete,
|
|
40
|
+
"mneme" => :render_mneme
|
|
89
41
|
}.freeze
|
|
90
42
|
private_constant :RENDER_DISPATCH
|
|
91
43
|
|
|
92
44
|
# Dispatches to the render method for the given view mode.
|
|
93
45
|
#
|
|
94
|
-
# @param mode [String] one of "basic", "verbose", "debug", "
|
|
46
|
+
# @param mode [String] one of "basic", "verbose", "debug", "melete", "mneme"
|
|
95
47
|
# @return [Hash, String, nil] structured message data (basic/verbose/debug),
|
|
96
|
-
# plain string (
|
|
48
|
+
# plain string (melete), or nil to hide the message
|
|
97
49
|
# @raise [ArgumentError] if the mode is not a valid view mode
|
|
98
50
|
def render(mode)
|
|
99
51
|
method = RENDER_DISPATCH[mode]
|
|
@@ -122,36 +74,31 @@ class MessageDecorator < ApplicationDecorator
|
|
|
122
74
|
render_basic
|
|
123
75
|
end
|
|
124
76
|
|
|
125
|
-
#
|
|
126
|
-
#
|
|
77
|
+
# Melete view — condensed single-line string for her message
|
|
78
|
+
# transcript. Returns nil to exclude from her context.
|
|
127
79
|
# Subclasses override to provide message-type-specific formatting.
|
|
128
80
|
# @return [String, nil] formatted transcript line, or nil to skip
|
|
129
|
-
def
|
|
81
|
+
def render_melete
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Mneme memory view — transcript line for eviction/context zones.
|
|
86
|
+
# Conversation and think messages return a prefixed string.
|
|
87
|
+
# Regular tool calls return +:tool_call+ (counter marker).
|
|
88
|
+
# Tool responses return +nil+ (silent).
|
|
89
|
+
# @return [String, Symbol, nil]
|
|
90
|
+
def render_mneme
|
|
130
91
|
nil
|
|
131
92
|
end
|
|
132
93
|
|
|
133
94
|
private
|
|
134
95
|
|
|
135
|
-
# Token count for display:
|
|
136
|
-
#
|
|
137
|
-
# so the TUI can prefix them with a tilde.
|
|
96
|
+
# Token count for display: heuristic estimate seeded by the
|
|
97
|
+
# {TokenEstimation} callback, refined later by {CountTokensJob}.
|
|
138
98
|
#
|
|
139
|
-
# @return [Hash] `{tokens: Integer
|
|
99
|
+
# @return [Hash] `{tokens: Integer}`
|
|
140
100
|
def token_info
|
|
141
|
-
|
|
142
|
-
if count > 0
|
|
143
|
-
{tokens: count, estimated: false}
|
|
144
|
-
else
|
|
145
|
-
{tokens: estimate_token_count, estimated: true}
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Delegates to the underlying object's heuristic token estimator.
|
|
150
|
-
# Both {Message} AR models and {MessagePayload} structs implement this.
|
|
151
|
-
#
|
|
152
|
-
# @return [Integer] at least 1
|
|
153
|
-
def estimate_token_count
|
|
154
|
-
object.estimate_tokens
|
|
101
|
+
{tokens: token_count.to_i}
|
|
155
102
|
end
|
|
156
103
|
|
|
157
104
|
# Extracts display content from the message payload.
|
|
@@ -173,7 +120,7 @@ class MessageDecorator < ApplicationDecorator
|
|
|
173
120
|
end
|
|
174
121
|
|
|
175
122
|
# Truncates long text by cutting the middle, preserving the start and end
|
|
176
|
-
# so context and conclusions aren't lost. Used for
|
|
123
|
+
# so context and conclusions aren't lost. Used for Melete transcripts where
|
|
177
124
|
# both the opening (intent) and closing (result) matter.
|
|
178
125
|
#
|
|
179
126
|
# @param text [String, nil] text to truncate
|
|
@@ -188,20 +135,4 @@ class MessageDecorator < ApplicationDecorator
|
|
|
188
135
|
tail = keep - head
|
|
189
136
|
"#{str[0, head]}#{MIDDLE_TRUNCATION_MARKER}#{str[-tail, tail]}"
|
|
190
137
|
end
|
|
191
|
-
|
|
192
|
-
# Normalizes input to something Draper can wrap.
|
|
193
|
-
# Message AR models pass through; hashes become MessagePayload structs
|
|
194
|
-
# with string-normalized keys.
|
|
195
|
-
def self.wrap_source(message)
|
|
196
|
-
return message unless message.is_a?(Hash)
|
|
197
|
-
|
|
198
|
-
normalized = message.transform_keys(&:to_s)
|
|
199
|
-
MessagePayload.new(
|
|
200
|
-
message_type: normalized["type"].to_s,
|
|
201
|
-
payload: normalized,
|
|
202
|
-
timestamp: normalized["timestamp"],
|
|
203
|
-
token_count: normalized["token_count"]&.to_i || 0
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
private_class_method :wrap_source
|
|
207
138
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared base for the three Melete-activation pending decorators (skill,
|
|
4
|
+
# workflow, goal). All three share the same TUI shape — a dimmed
|
|
5
|
+
# +pending_melete+ payload with +kind+ + +source+ + truncated content
|
|
6
|
+
# — and only differ on the +KIND+ constant and the per-type Melete
|
|
7
|
+
# transcript line. Subclasses override +KIND+ and +render_melete+; this
|
|
8
|
+
# base owns everything else.
|
|
9
|
+
class PendingFromMeleteDecorator < PendingMessageDecorator
|
|
10
|
+
# @return [nil] Melete activations are hidden in basic mode
|
|
11
|
+
def render_basic
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Hash] dimmed Melete-activation payload
|
|
16
|
+
def render_verbose
|
|
17
|
+
{
|
|
18
|
+
role: :pending_melete,
|
|
19
|
+
kind: self.class::KIND,
|
|
20
|
+
source: source_name,
|
|
21
|
+
content: truncate_lines(content, max_lines: 3),
|
|
22
|
+
status: "pending"
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Hash] full Melete-activation payload
|
|
27
|
+
def render_debug
|
|
28
|
+
{
|
|
29
|
+
role: :pending_melete,
|
|
30
|
+
kind: self.class::KIND,
|
|
31
|
+
source: source_name,
|
|
32
|
+
content: content,
|
|
33
|
+
status: "pending"
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Decorates a +from_melete_goal+ {PendingMessage} — a goal event Melete
|
|
4
|
+
# logged for the upcoming turn (created/updated/closed). See
|
|
5
|
+
# {PendingFromMeleteDecorator} for the shared TUI rendering shape.
|
|
6
|
+
class PendingFromMeleteGoalDecorator < PendingFromMeleteDecorator
|
|
7
|
+
KIND = "goal"
|
|
8
|
+
|
|
9
|
+
# @return [String] Melete transcript line — goal id and content
|
|
10
|
+
def render_melete
|
|
11
|
+
"Melete logged goal #{source_name}: #{truncate_middle(content)}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Decorates a +from_melete_skill+ {PendingMessage} — a skill that Melete
|
|
4
|
+
# activated for the upcoming turn. Promotes into a phantom
|
|
5
|
+
# +from_melete_skill+ tool_call/tool_response pair so the LLM sees it as
|
|
6
|
+
# its own past invocation; while pending, it shows in the TUI as a
|
|
7
|
+
# Melete badge so the user knows the skill is about to enter context.
|
|
8
|
+
#
|
|
9
|
+
# TUI rendering shape lives in {PendingFromMeleteDecorator} — only the
|
|
10
|
+
# +KIND+ constant and the Melete transcript line differ across the
|
|
11
|
+
# skill/workflow/goal trio.
|
|
12
|
+
class PendingFromMeleteSkillDecorator < PendingFromMeleteDecorator
|
|
13
|
+
KIND = "skill"
|
|
14
|
+
|
|
15
|
+
# @return [String] Melete transcript line (header only — content is the skill body)
|
|
16
|
+
def render_melete
|
|
17
|
+
"Melete activated skill: #{source_name}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Decorates a +from_melete_workflow+ {PendingMessage} — a workflow that
|
|
4
|
+
# Melete activated for the upcoming turn. See
|
|
5
|
+
# {PendingFromMeleteDecorator} for the shared TUI rendering shape.
|
|
6
|
+
class PendingFromMeleteWorkflowDecorator < PendingFromMeleteDecorator
|
|
7
|
+
KIND = "workflow"
|
|
8
|
+
|
|
9
|
+
# @return [String] Melete transcript line (header only — content is the workflow body)
|
|
10
|
+
def render_melete
|
|
11
|
+
"Melete activated workflow: #{source_name}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Decorates a +from_mneme+ {PendingMessage} — an associative recall
|
|
4
|
+
# enqueued by Mneme that will become a phantom +from_mneme+
|
|
5
|
+
# tool_call/tool_response pair on promotion. Background-kind, so it
|
|
6
|
+
# rides the next active drain instead of triggering one.
|
|
7
|
+
#
|
|
8
|
+
# Hidden in basic. Visible from verbose with a +[Mneme recall]+ badge.
|
|
9
|
+
class PendingFromMnemeDecorator < PendingMessageDecorator
|
|
10
|
+
# @return [nil] Mneme recalls are hidden in basic mode
|
|
11
|
+
def render_basic
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Hash] dimmed Mneme recall payload
|
|
16
|
+
def render_verbose
|
|
17
|
+
{
|
|
18
|
+
role: :pending_mneme,
|
|
19
|
+
content: truncate_lines(content, max_lines: 3),
|
|
20
|
+
status: "pending"
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Hash] full Mneme recall payload
|
|
25
|
+
def render_debug
|
|
26
|
+
{
|
|
27
|
+
role: :pending_mneme,
|
|
28
|
+
content: content,
|
|
29
|
+
status: "pending"
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String] Melete transcript line — Mneme recalls become part
|
|
34
|
+
# of Melete's extended-context view (her "what's about to land" peek).
|
|
35
|
+
def render_melete
|
|
36
|
+
"Mneme recalled (pending): #{truncate_middle(content)}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# +render_mneme+ is intentionally NOT overridden — Mneme runs recall
|
|
40
|
+
# over the conversation transcript, and surfacing pending Mneme
|
|
41
|
+
# recalls back to herself would create a circular injection where
|
|
42
|
+
# she keeps re-discovering her own queued contributions. Inherits
|
|
43
|
+
# the base nil so they stay invisible to her recall mode.
|
|
44
|
+
end
|