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
data/lib/anima/settings.rb
CHANGED
|
@@ -188,26 +188,12 @@ module Anima
|
|
|
188
188
|
value
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
-
# Regenerate session name every N messages.
|
|
192
|
-
# @return [Integer]
|
|
193
|
-
def name_generation_interval = get("session", "name_generation_interval")
|
|
194
|
-
|
|
195
191
|
# ─── Paths ───────────────────────────────────────────────────────
|
|
196
192
|
|
|
197
193
|
# Path to the soul file — the agent's self-authored identity.
|
|
198
194
|
# @return [String] absolute path
|
|
199
195
|
def soul_path = get("paths", "soul")
|
|
200
196
|
|
|
201
|
-
# ─── Environment ──────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
# Filenames to scan for in the working directory.
|
|
204
|
-
# @return [Array<String>]
|
|
205
|
-
def project_files_whitelist = get("environment", "project_files")
|
|
206
|
-
|
|
207
|
-
# Maximum directory depth for project file scanning.
|
|
208
|
-
# @return [Integer]
|
|
209
|
-
def project_files_max_depth = get("environment", "project_files_max_depth")
|
|
210
|
-
|
|
211
197
|
# ─── GitHub ─────────────────────────────────────────────────────
|
|
212
198
|
|
|
213
199
|
# Repository for feature requests (+owner/repo+ format).
|
|
@@ -219,23 +205,15 @@ module Anima
|
|
|
219
205
|
# @return [String]
|
|
220
206
|
def github_label = get("github", "label")
|
|
221
207
|
|
|
222
|
-
# ───
|
|
208
|
+
# ─── Melete ─────────────────────────────────────────
|
|
223
209
|
|
|
224
|
-
# Maximum tokens per
|
|
210
|
+
# Maximum tokens per Melete response.
|
|
225
211
|
# @return [Integer]
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
# Run the analytical brain synchronously before the main agent on user messages.
|
|
229
|
-
# @return [Boolean]
|
|
230
|
-
def analytical_brain_blocking_on_user_message = get("analytical_brain", "blocking_on_user_message")
|
|
231
|
-
|
|
232
|
-
# Run the analytical brain asynchronously after the main agent completes.
|
|
233
|
-
# @return [Boolean]
|
|
234
|
-
def analytical_brain_blocking_on_agent_message = get("analytical_brain", "blocking_on_agent_message")
|
|
212
|
+
def melete_max_tokens = get("melete", "max_tokens")
|
|
235
213
|
|
|
236
|
-
# Number of recent messages to include in
|
|
214
|
+
# Number of recent messages to include in Melete's context window.
|
|
237
215
|
# @return [Integer]
|
|
238
|
-
def
|
|
216
|
+
def melete_message_window = get("melete", "message_window")
|
|
239
217
|
|
|
240
218
|
# ─── Mneme (Memory Department) ────────────────────────────────
|
|
241
219
|
|
|
@@ -243,9 +221,9 @@ module Anima
|
|
|
243
221
|
# @return [Integer]
|
|
244
222
|
def mneme_max_tokens = get("mneme", "max_tokens")
|
|
245
223
|
|
|
246
|
-
# Fraction of the main
|
|
224
|
+
# Fraction of the main token budget for Mneme's eviction zone.
|
|
247
225
|
# @return [Float]
|
|
248
|
-
def
|
|
226
|
+
def eviction_fraction = get("mneme", "eviction_fraction")
|
|
249
227
|
|
|
250
228
|
# Fraction of the main viewport token budget reserved for L1 snapshots.
|
|
251
229
|
# @return [Float]
|
|
@@ -259,10 +237,6 @@ module Anima
|
|
|
259
237
|
# @return [Integer]
|
|
260
238
|
def mneme_l2_snapshot_threshold = get("mneme", "l2_snapshot_threshold")
|
|
261
239
|
|
|
262
|
-
# Fraction of the viewport to evict in batch when Mneme runs.
|
|
263
|
-
# @return [Float]
|
|
264
|
-
def mneme_eviction_fraction = get("mneme", "eviction_fraction")
|
|
265
|
-
|
|
266
240
|
# Fraction of the main viewport token budget reserved for pinned messages.
|
|
267
241
|
# Pinned messages appear between snapshots and the sliding window.
|
|
268
242
|
# @return [Float]
|
data/lib/anima/version.rb
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted when the Anthropic provider rejects the configured token with
|
|
5
|
+
# an authentication error. Surfaces to the TUI via
|
|
6
|
+
# {Events::Subscribers::AuthenticationBroadcaster} so the user is
|
|
7
|
+
# prompted for a new token — and to the conversation as a system
|
|
8
|
+
# message so the failure is visible in history.
|
|
9
|
+
#
|
|
10
|
+
# Not persisted — not included in {Message::TYPES}.
|
|
11
|
+
class AuthenticationRequired < Base
|
|
12
|
+
TYPE = "authentication_required"
|
|
13
|
+
|
|
14
|
+
# @param session_id [Integer] session the failure is scoped to
|
|
15
|
+
# @param content [String] human-readable error text from the provider
|
|
16
|
+
def initialize(session_id:, content:)
|
|
17
|
+
super(content: content, session_id: session_id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def type
|
|
21
|
+
TYPE
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/events/bounce_back.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Events
|
|
4
|
-
# Transient failure event emitted
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# Transient failure event emitted by {DrainJob} when the first LLM
|
|
5
|
+
# call on a bounce-back-flagged user_message fails. The promoted
|
|
6
|
+
# Message is destroyed so the TUI can remove the phantom and restore
|
|
7
|
+
# the user's text to the input field.
|
|
8
8
|
#
|
|
9
9
|
# Not persisted — not included in {Message::TYPES}.
|
|
10
10
|
class BounceBack < Base
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted after Mneme advances the boundary past an eviction zone.
|
|
5
|
+
# Subscribers broadcast the cutoff to connected clients so they can
|
|
6
|
+
# drop messages below it.
|
|
7
|
+
class EvictionCompleted
|
|
8
|
+
TYPE = "eviction.completed"
|
|
9
|
+
|
|
10
|
+
attr_reader :session_id, :evict_above_id
|
|
11
|
+
|
|
12
|
+
# @param session_id [Integer] the session whose boundary advanced
|
|
13
|
+
# @param evict_above_id [Integer] last message ID in the evicted zone;
|
|
14
|
+
# clients drop all messages with id <= this value
|
|
15
|
+
def initialize(session_id:, evict_above_id:)
|
|
16
|
+
@session_id = session_id
|
|
17
|
+
@evict_above_id = evict_above_id
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def event_name
|
|
21
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{type: TYPE, session_id:, evict_above_id:}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted after a {Goal} record is committed for the first time.
|
|
5
|
+
# The drain pipeline's {MeleteEnrichmentJob} subscribes for the
|
|
6
|
+
# duration of one Melete run so a fresh goal triggers Mneme recall
|
|
7
|
+
# against the updated goal set before the user's message reaches the LLM.
|
|
8
|
+
class GoalCreated
|
|
9
|
+
TYPE = "goal.created"
|
|
10
|
+
|
|
11
|
+
attr_reader :session_id, :goal_id
|
|
12
|
+
|
|
13
|
+
# @param session_id [Integer] session that owns the goal
|
|
14
|
+
# @param goal_id [Integer] the newly created goal
|
|
15
|
+
def initialize(session_id:, goal_id:)
|
|
16
|
+
@session_id = session_id
|
|
17
|
+
@goal_id = goal_id
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def event_name
|
|
21
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{type: TYPE, session_id:, goal_id:}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted after a {Goal}'s description is changed and the change is
|
|
5
|
+
# committed. Status-only updates (finish_goal, cascade completion,
|
|
6
|
+
# mark_goal_completed) do not emit — a completed goal carries no new
|
|
7
|
+
# search seed for Mneme.
|
|
8
|
+
#
|
|
9
|
+
# The drain pipeline's {MeleteEnrichmentJob} subscribes for the
|
|
10
|
+
# duration of one Melete run so a refined goal triggers Mneme recall
|
|
11
|
+
# against the updated wording before the user's message reaches the LLM.
|
|
12
|
+
class GoalUpdated
|
|
13
|
+
TYPE = "goal.updated"
|
|
14
|
+
|
|
15
|
+
attr_reader :session_id, :goal_id
|
|
16
|
+
|
|
17
|
+
# @param session_id [Integer] session that owns the goal
|
|
18
|
+
# @param goal_id [Integer] the updated goal
|
|
19
|
+
def initialize(session_id:, goal_id:)
|
|
20
|
+
@session_id = session_id
|
|
21
|
+
@goal_id = goal_id
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def event_name
|
|
25
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{type: TYPE, session_id:, goal_id:}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted by the drain loop after a single LLM round-trip completes.
|
|
5
|
+
# Carries the raw Anthropic response so downstream subscribers can
|
|
6
|
+
# persist messages, transition session state, and dispatch tool
|
|
7
|
+
# execution when the response is a +tool_use+.
|
|
8
|
+
#
|
|
9
|
+
# The drain loop hands off via this event — it does not persist
|
|
10
|
+
# Messages or release the session itself. Single responsibility:
|
|
11
|
+
# one subscriber pumps PendingMessages into the LLM, another owns
|
|
12
|
+
# the aftermath.
|
|
13
|
+
class LLMResponded
|
|
14
|
+
TYPE = "session.llm_responded"
|
|
15
|
+
|
|
16
|
+
attr_reader :session_id, :response, :api_metrics
|
|
17
|
+
|
|
18
|
+
# @param session_id [Integer] session that made the LLM call
|
|
19
|
+
# @param response [Hash] raw Anthropic response (with +content+ and +stop_reason+)
|
|
20
|
+
# @param api_metrics [Hash, nil] rate-limit and usage metrics from the provider
|
|
21
|
+
def initialize(session_id:, response:, api_metrics: nil)
|
|
22
|
+
@session_id = session_id
|
|
23
|
+
@response = response
|
|
24
|
+
@api_metrics = api_metrics
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def event_name
|
|
28
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{type: TYPE, session_id:, response:, api_metrics:}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted after a Message record is committed to the database.
|
|
5
|
+
# Subscribers react to persisted messages — not to raw domain events.
|
|
6
|
+
#
|
|
7
|
+
# Carries the Message record directly so subscribers don't need to
|
|
8
|
+
# look it up again.
|
|
9
|
+
class MessageCreated
|
|
10
|
+
TYPE = "message.created"
|
|
11
|
+
|
|
12
|
+
attr_reader :message
|
|
13
|
+
|
|
14
|
+
# @param message [Message] the persisted message record
|
|
15
|
+
def initialize(message)
|
|
16
|
+
@message = message
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def event_name
|
|
20
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{type: TYPE, message:}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted after a Message record is updated and committed.
|
|
5
|
+
# Used by subscribers that need to react to message changes
|
|
6
|
+
# (e.g. broadcasting updated token counts to WebSocket clients).
|
|
7
|
+
class MessageUpdated
|
|
8
|
+
TYPE = "message.updated"
|
|
9
|
+
|
|
10
|
+
attr_reader :message
|
|
11
|
+
|
|
12
|
+
# @param message [Message] the updated message record
|
|
13
|
+
def initialize(message)
|
|
14
|
+
@message = message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def event_name
|
|
18
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{type: TYPE, message:}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted when a session's transport-level state changes. Carries the
|
|
5
|
+
# AASM state after a transition (+"idle"+/+"awaiting"+/+"executing"+)
|
|
6
|
+
# or a transient UI signal (+"interrupting"+).
|
|
7
|
+
#
|
|
8
|
+
# Subscribers broadcast the state over ActionCable so the TUI spinner
|
|
9
|
+
# and sub-agent HUD update in sync.
|
|
10
|
+
class SessionStateChanged
|
|
11
|
+
TYPE = "session.state_changed"
|
|
12
|
+
|
|
13
|
+
attr_reader :session_id, :state
|
|
14
|
+
|
|
15
|
+
# @param session_id [Integer] the session the state change belongs to
|
|
16
|
+
# @param state [String] transport state name
|
|
17
|
+
def initialize(session_id:, state:)
|
|
18
|
+
@session_id = session_id
|
|
19
|
+
@state = state
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def event_name
|
|
23
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{type: TYPE, session_id: session_id, state: state}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted after {Session#activate_skill} enqueues a skill's phantom
|
|
5
|
+
# pair. Subscribers rebroadcast the session's active skills/workflow
|
|
6
|
+
# so the HUD reflects the new activation immediately (before the
|
|
7
|
+
# pending message even promotes).
|
|
8
|
+
class SkillActivated
|
|
9
|
+
TYPE = "skill.activated"
|
|
10
|
+
|
|
11
|
+
attr_reader :session_id, :skill_name
|
|
12
|
+
|
|
13
|
+
# @param session_id [Integer] the session the skill was activated on
|
|
14
|
+
# @param skill_name [String] canonical skill name
|
|
15
|
+
def initialize(session_id:, skill_name:)
|
|
16
|
+
@session_id = session_id
|
|
17
|
+
@skill_name = skill_name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def event_name
|
|
21
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{type: TYPE, session_id:, skill_name:}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted when a +user_message+ PendingMessage lands on an idle session.
|
|
5
|
+
# Melete subscribes via {Events::Subscribers::MeleteKickoff} and runs
|
|
6
|
+
# its enrichment loop — activating skills, reading workflows, refining
|
|
7
|
+
# goals, renaming the session — then either:
|
|
8
|
+
#
|
|
9
|
+
# * emits {Events::StartMneme} when a goal changed during the run, so
|
|
10
|
+
# Mneme can recall against the fresh goal set, or
|
|
11
|
+
# * emits {Events::StartProcessing} when goals were untouched, skipping
|
|
12
|
+
# Mneme entirely (no new search seed to recall against).
|
|
13
|
+
#
|
|
14
|
+
# First stage of the +start_melete → (start_mneme) → start_processing+
|
|
15
|
+
# chain that orchestrates context enrichment before the LLM is called.
|
|
16
|
+
class StartMelete
|
|
17
|
+
TYPE = "session.start_melete"
|
|
18
|
+
|
|
19
|
+
attr_reader :session_id, :pending_message_id
|
|
20
|
+
|
|
21
|
+
# @param session_id [Integer] session whose enrichment chain should continue
|
|
22
|
+
# @param pending_message_id [Integer, nil] the PendingMessage that triggered the chain
|
|
23
|
+
def initialize(session_id:, pending_message_id: nil)
|
|
24
|
+
@session_id = session_id
|
|
25
|
+
@pending_message_id = pending_message_id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def event_name
|
|
29
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{type: TYPE, session_id:, pending_message_id:}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted by {MeleteEnrichmentJob} when goals changed during the Melete
|
|
5
|
+
# run, signalling that Mneme should recall against the fresh goal set.
|
|
6
|
+
# Mneme subscribes via {Events::Subscribers::MnemeKickoff}, performs
|
|
7
|
+
# associative recall, enqueues its memories as background PendingMessages,
|
|
8
|
+
# and emits {Events::StartProcessing} to continue the drain.
|
|
9
|
+
#
|
|
10
|
+
# Second stage of the +start_melete → (start_mneme) → start_processing+
|
|
11
|
+
# chain. Conditional — when goals are untouched the pipeline jumps
|
|
12
|
+
# straight from {Events::StartMelete} to {Events::StartProcessing}.
|
|
13
|
+
class StartMneme
|
|
14
|
+
TYPE = "session.start_mneme"
|
|
15
|
+
|
|
16
|
+
attr_reader :session_id, :pending_message_id
|
|
17
|
+
|
|
18
|
+
# @param session_id [Integer] session whose drain pipeline should start
|
|
19
|
+
# @param pending_message_id [Integer] the PendingMessage that triggered the chain
|
|
20
|
+
def initialize(session_id:, pending_message_id:)
|
|
21
|
+
@session_id = session_id
|
|
22
|
+
@pending_message_id = pending_message_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def event_name
|
|
26
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{type: TYPE, session_id:, pending_message_id:}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted when an active PendingMessage lands on an idle session and does
|
|
5
|
+
# not require the Melete/Mneme enrichment pipeline (tool calls, tool
|
|
6
|
+
# responses, sub-agent replies), or when {MeleteEnrichmentJob} finishes
|
|
7
|
+
# without a goal change, or when {MnemeEnrichmentJob} finishes recall.
|
|
8
|
+
# The drain loop subscribes and begins processing the mailbox.
|
|
9
|
+
#
|
|
10
|
+
# Final stage of the +start_melete → (start_mneme) → start_processing+
|
|
11
|
+
# chain.
|
|
12
|
+
class StartProcessing
|
|
13
|
+
TYPE = "session.start_processing"
|
|
14
|
+
|
|
15
|
+
attr_reader :session_id, :pending_message_id
|
|
16
|
+
|
|
17
|
+
# @param session_id [Integer] session whose drain loop should start
|
|
18
|
+
# @param pending_message_id [Integer, nil] the PendingMessage that triggered the chain, if any
|
|
19
|
+
def initialize(session_id:, pending_message_id: nil)
|
|
20
|
+
@session_id = session_id
|
|
21
|
+
@pending_message_id = pending_message_id
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def event_name
|
|
25
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{type: TYPE, session_id:, pending_message_id:}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
# Emitted when {Mneme::Runner} advances the boundary past every remaining
|
|
5
|
+
# trace of a sub-agent — the spawn pair plus every +from_{nickname}+
|
|
6
|
+
# phantom pair. Subscribers broadcast the removal so clients drop the
|
|
7
|
+
# entry from the HUD panel.
|
|
8
|
+
#
|
|
9
|
+
# +session_id+ is the parent session (HUD owner), +child_id+ is the
|
|
10
|
+
# sub-agent session whose traces just aged out.
|
|
11
|
+
class SubagentEvicted
|
|
12
|
+
TYPE = "subagent.evicted"
|
|
13
|
+
|
|
14
|
+
attr_reader :session_id, :child_id
|
|
15
|
+
|
|
16
|
+
# @param session_id [Integer] parent session whose HUD should drop the entry
|
|
17
|
+
# @param child_id [Integer] sub-agent session whose traces were evicted
|
|
18
|
+
def initialize(session_id:, child_id:)
|
|
19
|
+
@session_id = session_id
|
|
20
|
+
@child_id = child_id
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def event_name
|
|
24
|
+
"#{Bus::NAMESPACE}.#{TYPE}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{type: TYPE, session_id:, child_id:}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
module Subscribers
|
|
5
|
+
# Rebroadcasts the session's active skills and workflow whenever the
|
|
6
|
+
# set can change: skill activation, workflow activation, or Mneme
|
|
7
|
+
# eviction. Same handler, three triggers — each event carries a
|
|
8
|
+
# +session_id+ and the broadcaster reads live state off the session.
|
|
9
|
+
#
|
|
10
|
+
# @example Registering at boot
|
|
11
|
+
# trigger = ->(event) {
|
|
12
|
+
# %w[anima.skill.activated anima.workflow.activated anima.eviction.completed]
|
|
13
|
+
# .include?(event[:name])
|
|
14
|
+
# }
|
|
15
|
+
# Events::Bus.subscribe(Events::Subscribers::ActiveStateBroadcaster.new, &trigger)
|
|
16
|
+
class ActiveStateBroadcaster
|
|
17
|
+
include Events::Subscriber
|
|
18
|
+
|
|
19
|
+
# @param event [Hash] Rails.event notification hash
|
|
20
|
+
def emit(event)
|
|
21
|
+
session_id = event.dig(:payload, :session_id)
|
|
22
|
+
session = Session.find_by(id: session_id)
|
|
23
|
+
session&.broadcast_active_state!
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
module Subscribers
|
|
5
|
+
# Reacts to {Events::AuthenticationRequired} by surfacing the provider
|
|
6
|
+
# rejection to the operator. Emits a +system_message+ into the
|
|
7
|
+
# conversation (so the failure lives in history) and broadcasts an
|
|
8
|
+
# +authentication_required+ frame on the session's ActionCable stream
|
|
9
|
+
# (so the TUI can prompt for a new token).
|
|
10
|
+
#
|
|
11
|
+
# Follows the same shape as {SessionStateBroadcaster}: jobs emit
|
|
12
|
+
# typed events, broadcasters own the ActionCable side.
|
|
13
|
+
class AuthenticationBroadcaster
|
|
14
|
+
include Events::Subscriber
|
|
15
|
+
|
|
16
|
+
# @param event [Hash] Rails.event notification hash
|
|
17
|
+
def emit(event)
|
|
18
|
+
payload = event[:payload]
|
|
19
|
+
session_id = payload[:session_id]
|
|
20
|
+
message = payload[:content]
|
|
21
|
+
|
|
22
|
+
Events::Bus.emit(Events::SystemMessage.new(
|
|
23
|
+
content: "Authentication failed: #{message}",
|
|
24
|
+
session_id: session_id
|
|
25
|
+
))
|
|
26
|
+
|
|
27
|
+
ActionCable.server.broadcast(
|
|
28
|
+
"session_#{session_id}",
|
|
29
|
+
{"action" => "authentication_required", "message" => message}
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
module Subscribers
|
|
5
|
+
# Entry subscriber for the drain loop. On {Events::StartProcessing},
|
|
6
|
+
# enqueues {DrainJob} — the actual work (session claim, PM promotion,
|
|
7
|
+
# LLM call) happens in the job so the emitter's thread isn't blocked.
|
|
8
|
+
class DrainKickoff
|
|
9
|
+
include Events::Subscriber
|
|
10
|
+
|
|
11
|
+
# @param event [Hash] Rails.event notification hash
|
|
12
|
+
def emit(event)
|
|
13
|
+
session_id = event[:payload][:session_id]
|
|
14
|
+
return unless session_id
|
|
15
|
+
|
|
16
|
+
DrainJob.perform_later(session_id)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Events
|
|
4
|
+
module Subscribers
|
|
5
|
+
# Broadcasts eviction cutoff to connected WebSocket clients after Mneme
|
|
6
|
+
# advances the boundary. Clients drop all messages above the cutoff
|
|
7
|
+
# (id <= evict_above_id) — older messages at the top of the chat view.
|
|
8
|
+
#
|
|
9
|
+
# @example Registering at boot
|
|
10
|
+
# Events::Bus.subscribe(Events::Subscribers::EvictionBroadcaster.new) { |event|
|
|
11
|
+
# event[:name] == "anima.eviction.completed"
|
|
12
|
+
# }
|
|
13
|
+
class EvictionBroadcaster
|
|
14
|
+
include Events::Subscriber
|
|
15
|
+
|
|
16
|
+
# @param event [Hash] Rails.event notification hash
|
|
17
|
+
def emit(event)
|
|
18
|
+
payload = event[:payload]
|
|
19
|
+
ActionCable.server.broadcast(
|
|
20
|
+
"session_#{payload[:session_id]}",
|
|
21
|
+
{"action" => "eviction", "evict_above_id" => payload[:evict_above_id]}
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|