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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +18 -20
  3. data/README.md +61 -95
  4. data/agents/thoughts-analyzer.md +12 -7
  5. data/anima-core.gemspec +1 -0
  6. data/app/channels/session_channel.rb +38 -58
  7. data/app/decorators/agent_message_decorator.rb +7 -2
  8. data/app/decorators/message_decorator.rb +31 -100
  9. data/app/decorators/pending_from_melete_decorator.rb +36 -0
  10. data/app/decorators/pending_from_melete_goal_decorator.rb +13 -0
  11. data/app/decorators/pending_from_melete_skill_decorator.rb +19 -0
  12. data/app/decorators/pending_from_melete_workflow_decorator.rb +13 -0
  13. data/app/decorators/pending_from_mneme_decorator.rb +44 -0
  14. data/app/decorators/pending_message_decorator.rb +94 -0
  15. data/app/decorators/pending_subagent_decorator.rb +46 -0
  16. data/app/decorators/pending_tool_response_decorator.rb +51 -0
  17. data/app/decorators/pending_user_message_decorator.rb +22 -0
  18. data/app/decorators/system_message_decorator.rb +5 -0
  19. data/app/decorators/tool_call_decorator.rb +13 -2
  20. data/app/decorators/tool_response_decorator.rb +2 -2
  21. data/app/decorators/user_message_decorator.rb +7 -2
  22. data/app/jobs/count_tokens_job.rb +23 -0
  23. data/app/jobs/drain_job.rb +169 -0
  24. data/app/jobs/melete_enrichment_job/goal_change_listener.rb +52 -0
  25. data/app/jobs/melete_enrichment_job.rb +48 -0
  26. data/app/jobs/mneme_enrichment_job.rb +46 -0
  27. data/app/jobs/tool_execution_job.rb +87 -0
  28. data/app/models/concerns/token_estimation.rb +54 -0
  29. data/app/models/goal.rb +21 -10
  30. data/app/models/message.rb +47 -36
  31. data/app/models/pending_message.rb +276 -29
  32. data/app/models/pinned_message.rb +8 -3
  33. data/app/models/session.rb +468 -432
  34. data/app/models/snapshot.rb +11 -21
  35. data/bin/inspect-cassette +17 -4
  36. data/config/application.rb +1 -0
  37. data/config/initializers/event_subscribers.rb +71 -4
  38. data/config/initializers/inflections.rb +3 -1
  39. data/db/cable_structure.sql +3 -3
  40. data/db/migrate/20260407170803_remove_viewport_message_ids_from_sessions.rb +5 -0
  41. data/db/migrate/20260407180400_remove_mneme_snapshot_pointer_columns_from_sessions.rb +6 -0
  42. data/db/migrate/20260411120553_add_token_count_to_pinned_messages.rb +5 -0
  43. data/db/migrate/20260411172926_remove_active_skills_and_workflow_from_sessions.rb +6 -0
  44. data/db/migrate/20260412110625_replace_processing_with_aasm_state.rb +6 -0
  45. data/db/migrate/20260418150323_add_kind_and_message_type_to_pending_messages.rb +6 -0
  46. data/db/migrate/20260419120000_add_drain_fields_to_pending_messages.rb +7 -0
  47. data/db/migrate/20260419130000_drop_pending_messages_kind_default.rb +5 -0
  48. data/db/migrate/20260419140000_add_drain_indexes_to_pending_messages.rb +8 -0
  49. data/db/migrate/20260420100000_add_hud_visibility_to_sessions.rb +15 -0
  50. data/db/queue_structure.sql +13 -13
  51. data/db/structure.sql +44 -31
  52. data/lib/agents/registry.rb +1 -1
  53. data/lib/anima/settings.rb +7 -33
  54. data/lib/anima/version.rb +1 -1
  55. data/lib/events/authentication_required.rb +24 -0
  56. data/lib/events/bounce_back.rb +4 -4
  57. data/lib/events/eviction_completed.rb +28 -0
  58. data/lib/events/goal_created.rb +28 -0
  59. data/lib/events/goal_updated.rb +32 -0
  60. data/lib/events/llm_responded.rb +35 -0
  61. data/lib/events/message_created.rb +27 -0
  62. data/lib/events/message_updated.rb +25 -0
  63. data/lib/events/session_state_changed.rb +30 -0
  64. data/lib/events/skill_activated.rb +28 -0
  65. data/lib/events/start_melete.rb +36 -0
  66. data/lib/events/start_mneme.rb +33 -0
  67. data/lib/events/start_processing.rb +32 -0
  68. data/lib/events/subagent_evicted.rb +31 -0
  69. data/lib/events/subscribers/active_state_broadcaster.rb +27 -0
  70. data/lib/events/subscribers/authentication_broadcaster.rb +34 -0
  71. data/lib/events/subscribers/drain_kickoff.rb +20 -0
  72. data/lib/events/subscribers/eviction_broadcaster.rb +26 -0
  73. data/lib/events/subscribers/llm_response_handler.rb +111 -0
  74. data/lib/events/subscribers/melete_kickoff.rb +24 -0
  75. data/lib/events/subscribers/message_broadcaster.rb +34 -0
  76. data/lib/events/subscribers/mneme_kickoff.rb +24 -0
  77. data/lib/events/subscribers/mneme_scheduler.rb +21 -0
  78. data/lib/events/subscribers/persister.rb +6 -8
  79. data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
  80. data/lib/events/subscribers/subagent_message_router.rb +26 -29
  81. data/lib/events/subscribers/subagent_visibility_broadcaster.rb +33 -0
  82. data/lib/events/subscribers/tool_response_creator.rb +33 -0
  83. data/lib/events/subscribers/transient_broadcaster.rb +1 -1
  84. data/lib/events/tool_executed.rb +34 -0
  85. data/lib/events/workflow_activated.rb +27 -0
  86. data/lib/llm/client.rb +41 -201
  87. data/lib/mcp/client_manager.rb +41 -46
  88. data/lib/mcp/stdio_transport.rb +9 -5
  89. data/lib/{analytical_brain → melete}/runner.rb +63 -68
  90. data/lib/{analytical_brain → melete}/tools/activate_skill.rb +1 -1
  91. data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +2 -2
  92. data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
  93. data/lib/{analytical_brain → melete}/tools/finish_goal.rb +3 -3
  94. data/lib/{analytical_brain → melete}/tools/goal_messaging.rb +4 -3
  95. data/lib/{analytical_brain → melete}/tools/read_workflow.rb +2 -2
  96. data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
  97. data/lib/{analytical_brain → melete}/tools/set_goal.rb +1 -1
  98. data/lib/{analytical_brain → melete}/tools/update_goal.rb +4 -4
  99. data/lib/{analytical_brain.rb → melete.rb} +6 -3
  100. data/lib/mneme/base_runner.rb +121 -0
  101. data/lib/mneme/l2_runner.rb +14 -20
  102. data/lib/mneme/recall_runner.rb +132 -0
  103. data/lib/mneme/runner.rb +118 -171
  104. data/lib/mneme/search.rb +104 -62
  105. data/lib/mneme/tools/nothing_to_surface.rb +25 -0
  106. data/lib/mneme/tools/save_snapshot.rb +2 -10
  107. data/lib/mneme/tools/surface_memory.rb +89 -0
  108. data/lib/mneme.rb +11 -5
  109. data/lib/shell_session.rb +287 -612
  110. data/lib/skills/definition.rb +2 -2
  111. data/lib/skills/registry.rb +1 -1
  112. data/lib/tools/base.rb +16 -0
  113. data/lib/tools/bash.rb +25 -57
  114. data/lib/tools/edit.rb +2 -0
  115. data/lib/tools/read.rb +2 -0
  116. data/lib/tools/registry.rb +79 -3
  117. data/lib/tools/{recall.rb → search_messages.rb} +19 -21
  118. data/lib/tools/spawn_specialist.rb +16 -10
  119. data/lib/tools/spawn_subagent.rb +20 -14
  120. data/lib/tools/subagent_prompts.rb +4 -4
  121. data/lib/tools/think.rb +1 -1
  122. data/lib/tools/{remember.rb → view_messages.rb} +10 -10
  123. data/lib/tools/write.rb +2 -0
  124. data/lib/tui/app.rb +5 -4
  125. data/lib/tui/braille_spinner.rb +7 -7
  126. data/lib/tui/decorators/base_decorator.rb +24 -3
  127. data/lib/tui/message_store.rb +93 -44
  128. data/lib/tui/screens/chat.rb +94 -20
  129. data/lib/tui/settings.rb +9 -2
  130. data/lib/workflows/definition.rb +3 -3
  131. data/lib/workflows/registry.rb +1 -1
  132. data/skills/github.md +38 -0
  133. data/templates/config.toml +4 -23
  134. data/workflows/review_pr.md +18 -14
  135. metadata +86 -28
  136. data/app/jobs/agent_request_job.rb +0 -199
  137. data/app/jobs/analytical_brain_job.rb +0 -33
  138. data/app/jobs/count_message_tokens_job.rb +0 -39
  139. data/app/jobs/passive_recall_job.rb +0 -24
  140. data/app/models/concerns/message/broadcasting.rb +0 -86
  141. data/lib/agent_loop.rb +0 -215
  142. data/lib/analytical_brain/tools/deactivate_skill.rb +0 -40
  143. data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -35
  144. data/lib/events/agent_message.rb +0 -25
  145. data/lib/events/subscribers/message_collector.rb +0 -64
  146. data/lib/events/tool_call.rb +0 -31
  147. data/lib/events/tool_response.rb +0 -33
  148. data/lib/mneme/compressed_viewport.rb +0 -204
  149. data/lib/mneme/passive_recall.rb +0 -138
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Broadcasts Message records to connected WebSocket clients via ActionCable.
4
- # Follows the Turbo Streams pattern: messages are broadcast on both create
5
- # and update, with an action type so clients can distinguish append from
6
- # replace operations.
7
- #
8
- # Each broadcast includes the Message's database ID, enabling clients to
9
- # maintain an ID-indexed store for efficient in-place updates (e.g. when
10
- # token counts arrive asynchronously from {CountMessageTokensJob}).
11
- #
12
- # When a new message pushes old messages out of the LLM's context window,
13
- # the broadcast includes `evicted_message_ids` so clients can remove
14
- # phantom messages that the agent no longer knows about.
15
- #
16
- # @example Create broadcast payload
17
- # {
18
- # "type" => "user_message", "content" => "hello", ...,
19
- # "id" => 42, "action" => "create",
20
- # "rendered" => { "basic" => { "role" => "user", "content" => "hello" } }
21
- # }
22
- #
23
- # @example Broadcast with viewport evictions
24
- # {
25
- # "type" => "agent_message", "content" => "...", ...,
26
- # "id" => 99, "action" => "create",
27
- # "evicted_message_ids" => [101, 102, 103]
28
- # }
29
- #
30
- # @example Update broadcast payload (e.g. token count arrives)
31
- # {
32
- # "type" => "user_message", "content" => "hello", ...,
33
- # "id" => 42, "action" => "update",
34
- # "rendered" => { "debug" => { "role" => "user", "content" => "hello", "tokens" => 15 } }
35
- # }
36
- module Message::Broadcasting
37
- extend ActiveSupport::Concern
38
-
39
- ACTION_CREATE = "create"
40
- ACTION_UPDATE = "update"
41
-
42
- included do
43
- after_create_commit :broadcast_create
44
- after_update_commit :broadcast_update
45
- end
46
-
47
- private
48
-
49
- def broadcast_create
50
- broadcast_message(action: ACTION_CREATE)
51
- end
52
-
53
- def broadcast_update
54
- broadcast_message(action: ACTION_UPDATE)
55
- end
56
-
57
- # Decorates the message for the session's current view mode and broadcasts
58
- # the payload to the session's ActionCable stream. Includes viewport
59
- # eviction metadata so clients can remove messages the LLM has forgotten.
60
- #
61
- # @param action [String] ACTION_CREATE or ACTION_UPDATE — tells clients how to handle the message
62
- def broadcast_message(action:)
63
- return unless session_id
64
-
65
- session = Session.find_by(id: session_id)
66
- return unless session
67
-
68
- mode = session.view_mode
69
- decorator = MessageDecorator.for(self)
70
- broadcast_payload = payload.merge("id" => id, "action" => action)
71
- broadcast_payload["api_metrics"] = api_metrics if api_metrics.present?
72
-
73
- if decorator
74
- broadcast_payload["rendered"] = {mode => decorator.render(mode)}
75
- end
76
-
77
- evicted_ids = session.recalculate_viewport!
78
- broadcast_payload["evicted_message_ids"] = evicted_ids if evicted_ids.any?
79
-
80
- # The nil? branch fires on every broadcast until boundary initializes, but
81
- # schedule_mneme! returns early after setting the boundary — cost is one DB read + write.
82
- session.schedule_mneme! if evicted_ids.any? || session.mneme_boundary_message_id.nil?
83
-
84
- ActionCable.server.broadcast("session_#{session_id}", broadcast_payload)
85
- end
86
- end
data/lib/agent_loop.rb DELETED
@@ -1,215 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "shellwords"
4
-
5
- # Orchestrates the LLM agent loop: accepts user input, runs the tool-use
6
- # cycle via {LLM::Client}, and emits events through {Events::Bus}.
7
- #
8
- # Extracted from {TUI::Screens::Chat} so the same agent logic can run from
9
- # the TUI, a background job, or an Action Cable channel.
10
- #
11
- # @note Not thread-safe. Callers must serialize concurrent access
12
- # (e.g. {AgentRequestJob} uses session-level processing locks).
13
- #
14
- # @example Basic usage
15
- # loop = AgentLoop.new(session: session)
16
- # loop.run
17
- # loop.finalize
18
- #
19
- # @example With dependency injection (testing)
20
- # loop = AgentLoop.new(session: session, client: mock_client, registry: mock_registry)
21
- # loop.run
22
- class AgentLoop
23
- # @return [Session] the conversation session this loop operates on
24
- attr_reader :session
25
-
26
- # @param session [Session] the conversation session
27
- # @param shell_session [ShellSession, nil] injectable persistent shell;
28
- # created automatically if not provided
29
- # @param client [LLM::Client, nil] injectable LLM client;
30
- # created lazily on first {#run} call if not provided
31
- # @param registry [Tools::Registry, nil] injectable tool registry;
32
- # built lazily on first {#run} call if not provided
33
- def initialize(session:, shell_session: nil, client: nil, registry: nil)
34
- @session = session
35
- @shell_session = shell_session || ShellSession.new(session_id: session.id)
36
- restore_initial_cwd
37
- @client = client
38
- @registry = registry
39
- end
40
-
41
- # Makes the first LLM API call to verify delivery. Called inside the
42
- # Bounce Back transaction — if this raises, the user event rolls back.
43
- #
44
- # Caches the first response so the subsequent {#run} call can continue
45
- # from it without duplicating the API call.
46
- #
47
- # @return [void]
48
- # @raise [Providers::Anthropic::Error] on any LLM delivery failure
49
- def deliver!
50
- @client ||= build_client
51
- @registry ||= build_tool_registry
52
-
53
- messages = @session.messages_for_llm
54
- options = build_llm_options
55
-
56
- @first_response = @client.provider.create_message(
57
- model: @client.model,
58
- messages: messages,
59
- max_tokens: @client.max_tokens,
60
- tools: @registry.schemas,
61
- include_metrics: true,
62
- **options
63
- )
64
- end
65
-
66
- # Runs the LLM tool-use loop on persisted session messages.
67
- #
68
- # When a cached first response exists (from {#deliver!}), continues
69
- # from that response without a redundant API call. Otherwise makes
70
- # a fresh call — used for pending message processing and the standard
71
- # path.
72
- #
73
- # Lets errors propagate — designed for callers like {AgentRequestJob}
74
- # that handle retries and need errors to bubble up.
75
- #
76
- # @return [String, nil] the agent's response text, or nil when interrupted
77
- # @raise [Providers::Anthropic::TransientError] on retryable network/server errors
78
- # @raise [Providers::Anthropic::AuthenticationError] on auth failures
79
- def run
80
- @client ||= build_client
81
- @registry ||= build_tool_registry
82
-
83
- messages = @session.messages_for_llm
84
- options = build_llm_options
85
-
86
- first_resp = @first_response
87
- @first_response = nil
88
-
89
- between_rounds = -> { @session.promote_pending_messages! }
90
-
91
- result = @client.chat_with_tools(
92
- messages, registry: @registry, session_id: @session.id,
93
- first_response: first_resp, between_rounds: between_rounds, **options
94
- )
95
- return unless result
96
-
97
- Events::Bus.emit(Events::AgentMessage.new(
98
- content: result[:text],
99
- session_id: @session.id,
100
- api_metrics: result[:api_metrics]
101
- ))
102
- result[:text]
103
- end
104
-
105
- # Clean up the underlying {ShellSession} PTY and resources.
106
- # Safe to call multiple times — subsequent calls are no-ops.
107
- def finalize
108
- @shell_session&.finalize
109
- end
110
-
111
- # Tool classes available to all sessions by default.
112
- # @return [Array<Class<Tools::Base>>]
113
- STANDARD_TOOLS = [Tools::Bash, Tools::Read, Tools::Write, Tools::Edit, Tools::WebGet, Tools::Think, Tools::Remember, Tools::Recall].freeze
114
-
115
- # Tools that bypass {Session#granted_tools} filtering.
116
- # The agent's reasoning depends on these regardless of task scope.
117
- # @return [Array<Class<Tools::Base>>]
118
- ALWAYS_GRANTED_TOOLS = [Tools::Think].freeze
119
-
120
- # Name-to-class mapping for tool restriction validation and registry building.
121
- # @return [Hash{String => Class<Tools::Base>}]
122
- STANDARD_TOOLS_BY_NAME = STANDARD_TOOLS.index_by(&:tool_name).freeze
123
-
124
- private
125
-
126
- # Restores the working directory inherited from the parent session.
127
- # Sub-agents store the parent's CWD at spawn time so their shell starts
128
- # in the same directory the parent was working in.
129
- # @return [void]
130
- def restore_initial_cwd
131
- cwd = @session.initial_cwd
132
- return unless cwd.present? && File.directory?(cwd)
133
-
134
- @shell_session.run("cd #{Shellwords.shellescape(cwd)}")
135
- end
136
-
137
- # Builds the LLM client with the appropriate model for this session type.
138
- # Sub-agents use a separate (typically cheaper) model from Settings.
139
- # @return [LLM::Client]
140
- def build_client
141
- if @session.sub_agent?
142
- LLM::Client.new(model: Anima::Settings.subagent_model)
143
- else
144
- LLM::Client.new
145
- end
146
- end
147
-
148
- # Assembles LLM options (system prompt).
149
- # Broadcasts the full debug context (system prompt + tool schemas)
150
- # to debug-mode TUI clients on every LLM request.
151
- # @return [Hash] options for {LLM::Client#chat_with_tools}
152
- def build_llm_options
153
- options = {}
154
- prompt = @session.system_prompt
155
- options[:system] = prompt if prompt
156
- @session.broadcast_debug_context(system: prompt, tools: @registry&.schemas)
157
- options
158
- end
159
-
160
- # Builds the tool registry appropriate for this session type.
161
- # Main sessions get standard tools + spawn_subagent + spawn_specialist.
162
- # Sub-agents get granted standard tools only (no spawning, no nesting).
163
- # Sub-agent results are delivered through natural text messages routed
164
- # by {Events::Subscribers::SubagentMessageRouter}.
165
- # When {Session#granted_tools} is nil, all standard tools are granted.
166
- # MCP tools from configured servers are registered for all session types.
167
- #
168
- # @return [Tools::Registry] registry with available tools
169
- def build_tool_registry
170
- context = {shell_session: @shell_session, session: @session}
171
- registry = Tools::Registry.new(context: context)
172
-
173
- granted_standard_tools.each { |tool| registry.register(tool) }
174
-
175
- if @session.sub_agent?
176
- registry.register(Tools::MarkGoalCompleted)
177
- else
178
- registry.register(Tools::SpawnSubagent)
179
- registry.register(Tools::SpawnSpecialist)
180
- registry.register(Tools::OpenIssue)
181
- end
182
-
183
- register_mcp_tools(registry)
184
-
185
- registry
186
- end
187
-
188
- # Loads tools from configured MCP servers and adds them to the registry.
189
- # Warnings are emitted as system messages — visible to both the user
190
- # (in verbose mode) and the LLM (via CONTEXT_TYPES) so the agent can
191
- # explain config issues instead of guessing.
192
- #
193
- # @param registry [Tools::Registry] the registry to add MCP tools to
194
- # @return [void]
195
- def register_mcp_tools(registry)
196
- warnings = Mcp::ClientManager.new.register_tools(registry)
197
- warnings.each do |message|
198
- Events::Bus.emit(Events::SystemMessage.new(content: message, session_id: @session.id))
199
- end
200
- end
201
-
202
- # Standard tools available to this session.
203
- # Returns all when {Session#granted_tools} is nil (no restriction).
204
- # Returns only matching tools when granted_tools is an array,
205
- # always including {ALWAYS_GRANTED_TOOLS}.
206
- #
207
- # @return [Array<Class<Tools::Base>>] tool classes to register
208
- def granted_standard_tools
209
- granted = @session.granted_tools
210
- return STANDARD_TOOLS unless granted
211
-
212
- explicitly_granted = granted.filter_map { |name| STANDARD_TOOLS_BY_NAME[name] }
213
- (ALWAYS_GRANTED_TOOLS + explicitly_granted).uniq
214
- end
215
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnalyticalBrain
4
- module Tools
5
- # Deactivates a domain knowledge skill on the main session.
6
- # The skill's recalled message stays in the conversation and
7
- # evicts naturally from the sliding window.
8
- class DeactivateSkill < ::Tools::Base
9
- def self.tool_name = "deactivate_skill"
10
-
11
- def self.description = "Remove domain knowledge that is no longer relevant."
12
-
13
- def self.input_schema
14
- {
15
- type: "object",
16
- properties: {
17
- skill_name: {type: "string"}
18
- },
19
- required: %w[skill_name]
20
- }
21
- end
22
-
23
- # @param main_session [Session] the session to deactivate the skill on
24
- def initialize(main_session:, **)
25
- @main_session = main_session
26
- end
27
-
28
- # @param input [Hash<String, Object>] with "skill_name" key
29
- # @return [String] confirmation message
30
- # @return [Hash] with :error key on validation failure
31
- def execute(input)
32
- skill_name = input["skill_name"].to_s.strip
33
- return {error: "Skill name cannot be blank"} if skill_name.empty?
34
-
35
- @main_session.deactivate_skill(skill_name)
36
- "Deactivated skill: #{skill_name}"
37
- end
38
- end
39
- end
40
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AnalyticalBrain
4
- module Tools
5
- # Deactivates the current workflow on the main session.
6
- # The workflow's recalled message stays in the conversation and
7
- # evicts naturally from the sliding window.
8
- class DeactivateWorkflow < ::Tools::Base
9
- def self.tool_name = "deactivate_workflow"
10
-
11
- def self.description = "Deactivate the current workflow when it is complete or no longer relevant."
12
-
13
- def self.input_schema
14
- {
15
- type: "object",
16
- properties: {},
17
- required: []
18
- }
19
- end
20
-
21
- # @param main_session [Session] the session to deactivate the workflow on
22
- def initialize(main_session:, **)
23
- @main_session = main_session
24
- end
25
-
26
- # @param input [Hash<String, Object>] (no parameters needed)
27
- # @return [String] confirmation message
28
- def execute(_input)
29
- previous = @main_session.active_workflow
30
- @main_session.deactivate_workflow
31
- previous ? "Deactivated workflow: #{previous}" : "No workflow was active"
32
- end
33
- end
34
- end
35
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Events
4
- class AgentMessage < Base
5
- TYPE = "agent_message"
6
-
7
- attr_reader :api_metrics
8
-
9
- # @param content [String] assistant response text
10
- # @param session_id [Integer, String] session identifier
11
- # @param api_metrics [Hash, nil] rate limits and usage from API response
12
- def initialize(content:, session_id: nil, api_metrics: nil)
13
- super(content: content, session_id: session_id)
14
- @api_metrics = api_metrics
15
- end
16
-
17
- def type
18
- TYPE
19
- end
20
-
21
- def to_h
22
- super.merge(api_metrics: api_metrics)
23
- end
24
- end
25
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Events
4
- module Subscribers
5
- # Collects chat-displayable events in-memory for the current session.
6
- # Provides the message list that the TUI renders and the LLM client consumes.
7
- #
8
- # Only user_message and agent_message events are collected — system_message,
9
- # tool_call, and tool_response are internal and not part of the chat display.
10
- #
11
- # @example
12
- # collector = Events::Subscribers::MessageCollector.new
13
- # Events::Bus.subscribe(collector)
14
- # collector.messages # => [{role: "user", content: "hi"}, ...]
15
- class MessageCollector
16
- include Events::Subscriber
17
-
18
- DISPLAYABLE_TYPES = %w[user_message agent_message].freeze
19
-
20
- # Maps event types to LLM-compatible role identifiers
21
- ROLE_MAP = {
22
- "user_message" => "user",
23
- "agent_message" => "assistant"
24
- }.freeze
25
-
26
- def initialize
27
- @messages = []
28
- @mutex = Mutex.new
29
- end
30
-
31
- # @return [Array<Hash>] thread-safe copy of collected messages
32
- def messages
33
- @mutex.synchronize { @messages.dup }
34
- end
35
-
36
- # Receives a Rails.event notification hash.
37
- # @param event [Hash] with :payload containing :type and :content keys
38
- def emit(event)
39
- type = event.dig(:payload, :type)
40
- return unless DISPLAYABLE_TYPES.include?(type)
41
-
42
- content = event.dig(:payload, :content)
43
- return if content.nil?
44
-
45
- @mutex.synchronize do
46
- @messages << {
47
- role: ROLE_MAP.fetch(type),
48
- content: content
49
- }
50
- end
51
- end
52
-
53
- # Directly push a pre-built message hash (used for loading persisted events).
54
- # @param message [Hash] with :role and :content keys
55
- def messages_push(message)
56
- @mutex.synchronize { @messages << message }
57
- end
58
-
59
- def clear
60
- @mutex.synchronize { @messages = [] }
61
- end
62
- end
63
- end
64
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Events
4
- class ToolCall < Base
5
- TYPE = "tool_call"
6
-
7
- attr_reader :tool_name, :tool_input, :tool_use_id, :timeout
8
-
9
- # @param content [String] human-readable description of the tool call
10
- # @param tool_name [String] registered tool name (e.g. "web_get")
11
- # @param tool_input [Hash] arguments passed to the tool
12
- # @param tool_use_id [String] Anthropic-assigned ID for correlating call/result
13
- # @param timeout [Integer] maximum seconds before the call is considered orphaned
14
- # @param session_id [String, nil] optional session identifier
15
- def initialize(content:, tool_name:, tool_input: {}, tool_use_id: nil, timeout: nil, session_id: nil)
16
- super(content: content, session_id: session_id)
17
- @tool_name = tool_name
18
- @tool_input = tool_input
19
- @tool_use_id = tool_use_id
20
- @timeout = timeout
21
- end
22
-
23
- def type
24
- TYPE
25
- end
26
-
27
- def to_h
28
- super.merge(tool_name: tool_name, tool_input: tool_input, tool_use_id: tool_use_id, timeout: timeout)
29
- end
30
- end
31
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Events
4
- class ToolResponse < Base
5
- TYPE = "tool_response"
6
-
7
- attr_reader :tool_name, :success, :tool_use_id
8
-
9
- # @param content [String] tool execution output
10
- # @param tool_name [String] registered tool name
11
- # @param success [Boolean] whether the tool executed successfully
12
- # @param tool_use_id [String, nil] Anthropic-assigned ID for correlating call/result
13
- # @param session_id [String, nil] optional session identifier
14
- def initialize(content:, tool_name:, success: true, tool_use_id: nil, session_id: nil)
15
- super(content: content, session_id: session_id)
16
- @tool_name = tool_name
17
- @success = success
18
- @tool_use_id = tool_use_id
19
- end
20
-
21
- def type
22
- TYPE
23
- end
24
-
25
- def success?
26
- @success
27
- end
28
-
29
- def to_h
30
- super.merge(tool_name: tool_name, success: success, tool_use_id: tool_use_id)
31
- end
32
- end
33
- end