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