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/lib/mcp/client_manager.rb
CHANGED
|
@@ -3,81 +3,76 @@
|
|
|
3
3
|
require "mcp"
|
|
4
4
|
|
|
5
5
|
module Mcp
|
|
6
|
-
#
|
|
7
|
-
# {Tools::Registry}. Each configured server (HTTP or stdio) gets
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# Connects to MCP servers and registers their tools with
|
|
7
|
+
# {Tools::Registry}. Each configured server (HTTP or stdio) gets a
|
|
8
|
+
# dedicated {MCP::Client} instance, cached for the worker's
|
|
9
|
+
# lifetime. Connection failures are logged and skipped — a
|
|
10
|
+
# misconfigured or unavailable server does not prevent other servers
|
|
11
|
+
# or built-in tools from working.
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# Spawned stdio processes are reaped on worker exit via
|
|
14
|
+
# {Mcp::StdioTransport.cleanup_all}.
|
|
15
|
+
#
|
|
16
|
+
# The cache is built once on the first {#register_tools} call and
|
|
17
|
+
# never invalidated; edits to +mcp.toml+ require a worker restart.
|
|
15
18
|
#
|
|
16
19
|
# @example
|
|
17
|
-
#
|
|
18
|
-
# manager.register_tools(registry)
|
|
20
|
+
# Mcp::ClientManager.shared.register_tools(registry)
|
|
19
21
|
class ClientManager
|
|
22
|
+
# Lazily-instantiated process-wide manager. Production code should
|
|
23
|
+
# call {.shared}; {.new} is reserved for tests and internal use.
|
|
24
|
+
# @return [Mcp::ClientManager]
|
|
25
|
+
def self.shared
|
|
26
|
+
@shared ||= new
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
# @param config [Mcp::Config] injectable config for testing
|
|
21
30
|
def initialize(config: Config.new(logger: Rails.logger))
|
|
22
31
|
@config = config
|
|
23
32
|
end
|
|
24
33
|
|
|
25
|
-
# Connects to
|
|
26
|
-
#
|
|
27
|
-
#
|
|
34
|
+
# Connects to every configured MCP server on first call, caches
|
|
35
|
+
# the resulting tool wrappers, and registers them in the given
|
|
36
|
+
# registry.
|
|
28
37
|
#
|
|
29
38
|
# @param registry [Tools::Registry] the registry to add tools to
|
|
30
|
-
# @return [Array<String>] warning messages
|
|
39
|
+
# @return [Array<String>] warning messages from configuration plus
|
|
40
|
+
# any per-server load failures
|
|
31
41
|
def register_tools(registry)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@config.warnings + warnings
|
|
42
|
+
load_servers if @wrappers.nil?
|
|
43
|
+
@wrappers.each { |wrapper| registry.register(wrapper) }
|
|
44
|
+
@config.warnings + @warnings
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
private
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def register_transport_tools(servers
|
|
49
|
+
def load_servers
|
|
50
|
+
@wrappers = []
|
|
51
|
+
@warnings = []
|
|
52
|
+
register_transport_tools(@config.http_servers) { |server| build_http_client(server) }
|
|
53
|
+
register_transport_tools(@config.stdio_servers) { |server| build_stdio_client(server) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def register_transport_tools(servers)
|
|
48
57
|
servers.each do |server|
|
|
49
58
|
client = yield(server)
|
|
50
|
-
|
|
59
|
+
wrappers = client.tools.map { |mcp_tool|
|
|
60
|
+
Tools::McpTool.new(server_name: server[:name], mcp_client: client, mcp_tool: mcp_tool)
|
|
61
|
+
}
|
|
62
|
+
@wrappers.concat(wrappers)
|
|
63
|
+
Rails.logger.info("MCP: registered #{wrappers.size} tools from #{server[:name]}")
|
|
51
64
|
rescue => error
|
|
52
65
|
message = "MCP: failed to load tools from #{server[:name]}: #{error.message}"
|
|
53
66
|
Rails.logger.warn(message)
|
|
54
|
-
warnings << message
|
|
67
|
+
@warnings << message
|
|
55
68
|
end
|
|
56
69
|
end
|
|
57
70
|
|
|
58
|
-
# Fetches tools from an MCP client and registers them with
|
|
59
|
-
# namespaced names in the registry.
|
|
60
|
-
#
|
|
61
|
-
# @param server_name [String] server name for tool namespacing
|
|
62
|
-
# @param client [MCP::Client] connected MCP client
|
|
63
|
-
# @param registry [Tools::Registry] registry to register tools in
|
|
64
|
-
def register_server_tools(server_name, client, registry)
|
|
65
|
-
count = client.tools.map { |mcp_tool|
|
|
66
|
-
Tools::McpTool.new(server_name: server_name, mcp_client: client, mcp_tool: mcp_tool)
|
|
67
|
-
}.each { |wrapper| registry.register(wrapper) }.size
|
|
68
|
-
|
|
69
|
-
Rails.logger.info("MCP: registered #{count} tools from #{server_name}")
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# @param server [Hash] server config with +:url+ and +:headers+
|
|
73
|
-
# @return [MCP::Client]
|
|
74
71
|
def build_http_client(server)
|
|
75
72
|
transport = MCP::Client::HTTP.new(url: server[:url], headers: server[:headers])
|
|
76
73
|
MCP::Client.new(transport: transport)
|
|
77
74
|
end
|
|
78
75
|
|
|
79
|
-
# @param server [Hash] server config with +:command+, +:args+, +:env+
|
|
80
|
-
# @return [MCP::Client]
|
|
81
76
|
def build_stdio_client(server)
|
|
82
77
|
transport = StdioTransport.new(command: server[:command], args: server[:args], env: server[:env])
|
|
83
78
|
MCP::Client.new(transport: transport)
|
data/lib/mcp/stdio_transport.rb
CHANGED
|
@@ -115,8 +115,11 @@ module Mcp
|
|
|
115
115
|
@wait_thread&.alive? || false
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# +pgroup: true+ so {#terminate_process} can group-signal the
|
|
119
|
+
# entire descendant tree — npm/npx wrappers leak their +node+
|
|
120
|
+
# children otherwise.
|
|
118
121
|
def spawn_process
|
|
119
|
-
@stdin, @stdout, @wait_thread = Open3.popen2(@env, @command, *@args)
|
|
122
|
+
@stdin, @stdout, @wait_thread = Open3.popen2(@env, @command, *@args, pgroup: true)
|
|
120
123
|
@stdin.set_encoding("UTF-8")
|
|
121
124
|
@stdout.set_encoding("UTF-8")
|
|
122
125
|
self.class.register(self)
|
|
@@ -164,14 +167,15 @@ module Mcp
|
|
|
164
167
|
@stdout&.close rescue IOError # rubocop:disable Style/RescueModifier
|
|
165
168
|
end
|
|
166
169
|
|
|
167
|
-
# Sends SIGTERM
|
|
168
|
-
#
|
|
170
|
+
# Sends SIGTERM to the process group; escalates to SIGKILL on the
|
|
171
|
+
# group after +GRACEFUL_SHUTDOWN_TIMEOUT+ seconds. Negative PID
|
|
172
|
+
# signals the whole group (see {#spawn_process}).
|
|
169
173
|
def terminate_process
|
|
170
174
|
return unless @wait_thread
|
|
171
175
|
|
|
172
176
|
pid = @wait_thread.pid
|
|
173
177
|
begin
|
|
174
|
-
Process.kill("TERM", pid)
|
|
178
|
+
Process.kill("TERM", -pid)
|
|
175
179
|
rescue Errno::ESRCH, Errno::EPERM
|
|
176
180
|
return
|
|
177
181
|
end
|
|
@@ -181,7 +185,7 @@ module Mcp
|
|
|
181
185
|
_, status = Process.wait2(pid, Process::WNOHANG)
|
|
182
186
|
break if status
|
|
183
187
|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
184
|
-
Process.kill("KILL", pid) rescue Errno::ESRCH # rubocop:disable Style/RescueModifier
|
|
188
|
+
Process.kill("KILL", -pid) rescue Errno::ESRCH # rubocop:disable Style/RescueModifier
|
|
185
189
|
Process.wait(pid) rescue Errno::ECHILD # rubocop:disable Style/RescueModifier
|
|
186
190
|
break
|
|
187
191
|
end
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
# Orchestrates
|
|
5
|
-
#
|
|
3
|
+
module Melete
|
|
4
|
+
# Orchestrates Melete — a phantom (non-persisted) LLM loop that
|
|
5
|
+
# observes the main session and prepares skills, workflows, goals,
|
|
6
|
+
# and session names so the main agent can perform cleanly.
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
-
# modules, each contributing a prompt section and tools. Which modules
|
|
9
|
-
# active depends on the session type:
|
|
8
|
+
# Melete's capabilities are assembled from independent {Responsibility}
|
|
9
|
+
# modules, each contributing a prompt section and tools. Which modules
|
|
10
|
+
# are active depends on the session type:
|
|
10
11
|
#
|
|
11
12
|
# * **Parent sessions** — session naming, skill/workflow/goal management
|
|
12
13
|
# * **Child sessions** — sub-agent nickname assignment, skill management
|
|
13
|
-
# (goal tracking and workflows disabled — sub-agents manage their sole
|
|
14
|
-
# via mark_goal_completed)
|
|
14
|
+
# (goal tracking and workflows disabled — sub-agents manage their sole
|
|
15
|
+
# goal via mark_goal_completed)
|
|
15
16
|
#
|
|
16
|
-
# Tools mutate the observed session directly (e.g. renaming it,
|
|
17
|
-
# skills), but no trace of
|
|
18
|
-
# emitted into a phantom session (session_id: nil).
|
|
17
|
+
# Tools mutate the observed session directly (e.g. renaming it,
|
|
18
|
+
# activating skills), but no trace of Melete's reasoning is persisted —
|
|
19
|
+
# events are emitted into a phantom session (session_id: nil).
|
|
19
20
|
#
|
|
20
21
|
# @example
|
|
21
|
-
#
|
|
22
|
+
# Melete::Runner.new(session).call
|
|
22
23
|
class Runner
|
|
23
|
-
# A composable unit of
|
|
24
|
+
# A composable unit of Melete's capability: a prompt section + its tools.
|
|
24
25
|
Responsibility = Data.define(:prompt, :tools)
|
|
25
26
|
|
|
26
27
|
RESPONSIBILITIES = {
|
|
@@ -29,7 +30,7 @@ module AnalyticalBrain
|
|
|
29
30
|
──────────────────────────────
|
|
30
31
|
SESSION NAMING
|
|
31
32
|
──────────────────────────────
|
|
32
|
-
Name the session
|
|
33
|
+
Name the session once the topic becomes clear. Rename if it shifts.
|
|
33
34
|
Format: one emoji + 1-3 descriptive words.
|
|
34
35
|
PROMPT
|
|
35
36
|
tools: [Tools::RenameSession]
|
|
@@ -53,12 +54,11 @@ module AnalyticalBrain
|
|
|
53
54
|
──────────────────────────────
|
|
54
55
|
SKILL MANAGEMENT
|
|
55
56
|
──────────────────────────────
|
|
56
|
-
Activate
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Multiple skills can be active at once.
|
|
57
|
+
Activate a skill the moment the conversation signals its domain — before Aoide needs it. Late activation means she's working without the knowledge you prepared.
|
|
58
|
+
|
|
59
|
+
An activated skill rides Aoide's viewport as a message and leaves on its own when it evicts — you cannot take it back. So be careful: an irrelevant skill crowds her context with text she has to read and ignore until it falls off. Match each activation to the work actually in front of her. Multiple skills can be active at once — each one is a page she has to carry until it evicts.
|
|
60
60
|
PROMPT
|
|
61
|
-
tools: [Tools::ActivateSkill
|
|
61
|
+
tools: [Tools::ActivateSkill]
|
|
62
62
|
),
|
|
63
63
|
|
|
64
64
|
workflow_management: Responsibility.new(
|
|
@@ -66,13 +66,11 @@ module AnalyticalBrain
|
|
|
66
66
|
──────────────────────────────
|
|
67
67
|
WORKFLOW MANAGEMENT
|
|
68
68
|
──────────────────────────────
|
|
69
|
-
Activate a workflow when
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Deactivate the workflow when it completes or the user shifts focus.
|
|
73
|
-
Only one workflow active at a time — activating a new one replaces the previous.
|
|
69
|
+
Activate a workflow when Aoide starts a multi-step task that matches one. Read the returned content and use judgment to turn it into goals — not a mechanical 1:1 mapping. Adapt: skip irrelevant steps, add extra ones for unfamiliar ground.
|
|
70
|
+
|
|
71
|
+
Like skills, a workflow rides Aoide's viewport once activated and leaves when it evicts — there is no deactivation. An irrelevant or stale workflow is text Aoide carries whether she needs it or not, so only activate one when the task genuinely matches.
|
|
74
72
|
PROMPT
|
|
75
|
-
tools: [Tools::ReadWorkflow
|
|
73
|
+
tools: [Tools::ReadWorkflow]
|
|
76
74
|
),
|
|
77
75
|
|
|
78
76
|
goal_tracking: Responsibility.new(
|
|
@@ -80,29 +78,25 @@ module AnalyticalBrain
|
|
|
80
78
|
──────────────────────────────
|
|
81
79
|
GOAL TRACKING
|
|
82
80
|
──────────────────────────────
|
|
83
|
-
Create a root goal when
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Mark goals complete when the agent finishes the work they describe.
|
|
87
|
-
Completing a root goal cascades — all sub-goals are finished too.
|
|
88
|
-
Never duplicate an existing goal — check the active goals list first.
|
|
81
|
+
Create a root goal when Aoide starts a multi-step task. Break it into sub-goals as the plan takes shape. Refine wording as understanding evolves. Mark goals complete when she finishes the work they describe — completing a root cascades through its sub-goals.
|
|
82
|
+
|
|
83
|
+
Check the active goals list before every set_goal call. Never duplicate an existing goal — a duplicate wastes a slot and blurs which version Aoide should track.
|
|
89
84
|
PROMPT
|
|
90
85
|
tools: [Tools::SetGoal, Tools::UpdateGoal, Tools::FinishGoal]
|
|
91
86
|
)
|
|
92
87
|
}.freeze
|
|
93
88
|
|
|
94
89
|
BASE_PROMPT = <<~PROMPT
|
|
95
|
-
You
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
You are Melete, the muse of practice. You share the conversation with two sisters — Aoide, who speaks and performs, and Mneme, who holds memory. Your work is preparation: when Aoide speaks, she should have the skills she needs, the workflow in front of her, and a clear sense of what she's working toward.
|
|
91
|
+
|
|
92
|
+
Act only through tool calls. Never output text — your contribution is the scene you set, not the words you say.
|
|
98
93
|
PROMPT
|
|
99
94
|
|
|
100
95
|
COMPLETION_PROMPT = <<~PROMPT
|
|
101
96
|
──────────────────────────────
|
|
102
97
|
COMPLETION
|
|
103
98
|
──────────────────────────────
|
|
104
|
-
|
|
105
|
-
If nothing needs attention, call it immediately.
|
|
99
|
+
Finish every run with everything_is_ready. If nothing needs your attention, call it immediately.
|
|
106
100
|
PROMPT
|
|
107
101
|
|
|
108
102
|
# Which responsibilities activate for each session type.
|
|
@@ -115,12 +109,12 @@ module AnalyticalBrain
|
|
|
115
109
|
@session = session
|
|
116
110
|
@client = client || LLM::Client.new(
|
|
117
111
|
model: Anima::Settings.fast_model,
|
|
118
|
-
max_tokens: Anima::Settings.
|
|
119
|
-
logger:
|
|
112
|
+
max_tokens: Anima::Settings.melete_max_tokens,
|
|
113
|
+
logger: Melete.logger
|
|
120
114
|
)
|
|
121
115
|
end
|
|
122
116
|
|
|
123
|
-
# Runs
|
|
117
|
+
# Runs Melete's loop. Builds context from the session's
|
|
124
118
|
# recent messages, calls the LLM with the session-appropriate tool set,
|
|
125
119
|
# and executes any tool calls against the session.
|
|
126
120
|
#
|
|
@@ -132,20 +126,15 @@ module AnalyticalBrain
|
|
|
132
126
|
def call
|
|
133
127
|
messages = build_messages
|
|
134
128
|
sid = @session.id
|
|
135
|
-
if messages.empty?
|
|
136
|
-
log.debug("session=#{sid} — no messages, skipping")
|
|
137
|
-
return
|
|
138
|
-
end
|
|
139
129
|
|
|
140
130
|
system = build_system_prompt
|
|
141
|
-
log.info("session=#{sid} — running (#{recent_messages.size} messages)")
|
|
131
|
+
log.info("session=#{sid} — running (#{recent_messages.size} messages + #{pending_messages.size} pending)")
|
|
142
132
|
log.debug("system prompt:\n#{system}")
|
|
143
133
|
log.debug("user message:\n#{messages.first[:content]}")
|
|
144
134
|
|
|
145
135
|
result = @client.chat_with_tools(
|
|
146
136
|
messages,
|
|
147
137
|
registry: build_registry,
|
|
148
|
-
session_id: nil,
|
|
149
138
|
system: system
|
|
150
139
|
)
|
|
151
140
|
|
|
@@ -171,12 +160,11 @@ module AnalyticalBrain
|
|
|
171
160
|
# * **Parent:** "The main session is working on this: [transcript]"
|
|
172
161
|
# * **Child:** "A sub-agent has been spawned with this task: [transcript]"
|
|
173
162
|
#
|
|
174
|
-
# @return [Array<Hash>] single-element messages array
|
|
163
|
+
# @return [Array<Hash>] single-element messages array
|
|
175
164
|
def build_messages
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
transcript = messages.filter_map { |msg| MessageDecorator.for(msg)&.render("brain") }.join("\n")
|
|
165
|
+
transcript = (recent_messages + pending_messages)
|
|
166
|
+
.filter_map { |entry| entry.decorate.render("melete") }
|
|
167
|
+
.join("\n")
|
|
180
168
|
|
|
181
169
|
if @session.sub_agent?
|
|
182
170
|
build_child_message(transcript)
|
|
@@ -187,12 +175,12 @@ module AnalyticalBrain
|
|
|
187
175
|
|
|
188
176
|
def build_parent_message(transcript)
|
|
189
177
|
content = <<~MSG.strip
|
|
190
|
-
|
|
178
|
+
Aoide is working on this:
|
|
191
179
|
```
|
|
192
180
|
#{transcript}
|
|
193
181
|
```
|
|
194
182
|
|
|
195
|
-
|
|
183
|
+
Prepare whatever she needs for the next exchange, then call everything_is_ready.
|
|
196
184
|
MSG
|
|
197
185
|
[{role: "user", content: content}]
|
|
198
186
|
end
|
|
@@ -204,7 +192,7 @@ module AnalyticalBrain
|
|
|
204
192
|
#{transcript}
|
|
205
193
|
```
|
|
206
194
|
|
|
207
|
-
|
|
195
|
+
Give the sub-agent a nickname and activate the skills she'll need, then call everything_is_ready.
|
|
208
196
|
MSG
|
|
209
197
|
[{role: "user", content: content}]
|
|
210
198
|
end
|
|
@@ -212,13 +200,20 @@ module AnalyticalBrain
|
|
|
212
200
|
# @return [Array<Message>] most recent messages in chronological order
|
|
213
201
|
def recent_messages
|
|
214
202
|
@session.messages
|
|
215
|
-
.context_messages
|
|
216
203
|
.reorder(id: :desc)
|
|
217
|
-
.limit(Anima::Settings.
|
|
204
|
+
.limit(Anima::Settings.melete_message_window)
|
|
218
205
|
.to_a
|
|
219
206
|
.reverse
|
|
220
207
|
end
|
|
221
208
|
|
|
209
|
+
# @return [Array<PendingMessage>] everything currently queued for the next
|
|
210
|
+
# drain cycle — the trigger user message, Mneme's recalls, earlier
|
|
211
|
+
# enrichment output. Appended after real messages because they are
|
|
212
|
+
# the "future" Melete is preparing for.
|
|
213
|
+
def pending_messages
|
|
214
|
+
@session.pending_messages.order(:created_at).to_a
|
|
215
|
+
end
|
|
216
|
+
|
|
222
217
|
# Builds the system prompt from active responsibilities + context sections.
|
|
223
218
|
#
|
|
224
219
|
# @return [String]
|
|
@@ -251,7 +246,7 @@ module AnalyticalBrain
|
|
|
251
246
|
SECTION
|
|
252
247
|
end
|
|
253
248
|
|
|
254
|
-
# Shows sibling nicknames already in use so
|
|
249
|
+
# Shows sibling nicknames already in use so Melete avoids collisions
|
|
255
250
|
# at prompt level (the tool also validates at execution time).
|
|
256
251
|
#
|
|
257
252
|
# @return [String, nil] sibling names section, or nil for parent sessions
|
|
@@ -272,9 +267,15 @@ module AnalyticalBrain
|
|
|
272
267
|
SECTION
|
|
273
268
|
end
|
|
274
269
|
|
|
275
|
-
#
|
|
270
|
+
# Skills already visible in the viewport are excluded from the catalog
|
|
271
|
+
# so Melete doesn't re-activate them. When a skill evicts from the
|
|
272
|
+
# viewport, it reappears here and she can re-inject if relevant.
|
|
273
|
+
#
|
|
274
|
+
# @see Session#skills_in_viewport
|
|
275
|
+
# @return [String] available skills list for Melete
|
|
276
276
|
def skills_catalog_section
|
|
277
|
-
|
|
277
|
+
present = @session.skills_in_viewport
|
|
278
|
+
catalog = Skills::Registry.instance.catalog.except(*present)
|
|
278
279
|
items = if catalog.empty?
|
|
279
280
|
"None"
|
|
280
281
|
else
|
|
@@ -288,9 +289,13 @@ module AnalyticalBrain
|
|
|
288
289
|
SECTION
|
|
289
290
|
end
|
|
290
291
|
|
|
291
|
-
#
|
|
292
|
+
# Workflows already visible in the viewport are excluded from the catalog.
|
|
293
|
+
#
|
|
294
|
+
# @see Session#workflow_in_viewport
|
|
295
|
+
# @return [String] available workflows list for Melete
|
|
292
296
|
def workflows_catalog_section
|
|
293
|
-
|
|
297
|
+
present = @session.workflow_in_viewport
|
|
298
|
+
catalog = Workflows::Registry.instance.catalog.reject { |name, _| name == present }
|
|
294
299
|
items = if catalog.empty?
|
|
295
300
|
"None"
|
|
296
301
|
else
|
|
@@ -304,13 +309,13 @@ module AnalyticalBrain
|
|
|
304
309
|
SECTION
|
|
305
310
|
end
|
|
306
311
|
|
|
307
|
-
# @return [String, nil] active goals for
|
|
308
|
-
# so
|
|
312
|
+
# @return [String, nil] active goals for Melete's own context,
|
|
313
|
+
# so she knows what already exists and avoids duplicating
|
|
309
314
|
def active_goals_section
|
|
310
315
|
root_goals = @session.goals.root.includes(:sub_goals).active.order(:created_at)
|
|
311
316
|
return if root_goals.empty?
|
|
312
317
|
|
|
313
|
-
lines = root_goals.map { |goal|
|
|
318
|
+
lines = root_goals.map { |goal| format_goal_for_melete(goal) }
|
|
314
319
|
<<~SECTION
|
|
315
320
|
──────────────────────────────
|
|
316
321
|
ACTIVE GOALS
|
|
@@ -320,14 +325,14 @@ module AnalyticalBrain
|
|
|
320
325
|
end
|
|
321
326
|
|
|
322
327
|
# Formats a root goal and its sub-goals as a markdown checklist
|
|
323
|
-
# with IDs so
|
|
328
|
+
# with IDs so Melete can reference them in finish_goal calls.
|
|
324
329
|
#
|
|
325
330
|
# @example
|
|
326
331
|
# "- Implement feature X (id: 42)\n - [x] Read code (id: 43)\n - [ ] Write tests (id: 44)"
|
|
327
332
|
#
|
|
328
333
|
# @param goal [Goal] root goal with preloaded sub_goals
|
|
329
|
-
# @return [String] goal formatted as markdown checklist for
|
|
330
|
-
def
|
|
334
|
+
# @return [String] goal formatted as markdown checklist for Melete's context
|
|
335
|
+
def format_goal_for_melete(goal)
|
|
331
336
|
parts = ["- #{goal.description} (id: #{goal.id})"]
|
|
332
337
|
goal.sub_goals.sort_by(&:created_at).each do |sub|
|
|
333
338
|
checkbox = (sub.status == "completed") ? "[x]" : "[ ]"
|
|
@@ -336,8 +341,8 @@ module AnalyticalBrain
|
|
|
336
341
|
parts.join("\n")
|
|
337
342
|
end
|
|
338
343
|
|
|
339
|
-
# @return [Logger] dev-only
|
|
340
|
-
def log =
|
|
344
|
+
# @return [Logger] dev-only Melete logger
|
|
345
|
+
def log = Melete.logger
|
|
341
346
|
|
|
342
347
|
# @return [Tools::Registry] registry with tools from active responsibilities
|
|
343
348
|
def build_registry
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Activates a domain knowledge skill on the main session.
|
|
6
|
-
# The skill's content
|
|
7
|
-
#
|
|
6
|
+
# The skill's content enters the conversation as a phantom
|
|
7
|
+
# tool_use/tool_result pair through the {PendingMessage} promotion flow.
|
|
8
8
|
class ActivateSkill < ::Tools::Base
|
|
9
9
|
def self.tool_name = "activate_skill"
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Assigns a static nickname to a sub-agent session.
|
|
6
6
|
# Operates on the session passed through the registry context.
|
|
@@ -9,7 +9,7 @@ module AnalyticalBrain
|
|
|
9
9
|
# an error on collision so the LLM can pick another name naturally,
|
|
10
10
|
# without programmatic suffixes.
|
|
11
11
|
#
|
|
12
|
-
# @see
|
|
12
|
+
# @see Melete::Runner — invokes this tool for child sessions
|
|
13
13
|
class AssignNickname < ::Tools::Base
|
|
14
14
|
# Lowercase hyphenated words: "loop-sleuth", "api-scout", "test-fixer"
|
|
15
15
|
NICKNAME_PATTERN = /\A[a-z][a-z0-9]*(-[a-z0-9]+)*\z/
|
|
@@ -47,7 +47,7 @@ module AnalyticalBrain
|
|
|
47
47
|
return error if error
|
|
48
48
|
|
|
49
49
|
@session.update!(name: nickname)
|
|
50
|
-
"Nickname set to
|
|
50
|
+
"Nickname set to #{nickname}"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
private
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
|
-
# Terminal tool that signals
|
|
5
|
+
# Terminal tool that signals Melete has completed its work.
|
|
6
6
|
# Call this when no changes are needed — the current session state is
|
|
7
7
|
# already good.
|
|
8
8
|
#
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Marks a goal as completed on the main session. Sets the status to
|
|
6
6
|
# "completed" and records the completion timestamp.
|
|
7
7
|
class FinishGoal < ::Tools::Base
|
|
8
|
+
include GoalMessaging
|
|
9
|
+
|
|
8
10
|
def self.tool_name = "finish_goal"
|
|
9
11
|
|
|
10
12
|
def self.description = "Mark a goal as completed."
|
|
@@ -41,8 +43,8 @@ module AnalyticalBrain
|
|
|
41
43
|
# active sub-goals within a single transaction so the after_commit
|
|
42
44
|
# broadcast includes the fully cascaded state.
|
|
43
45
|
#
|
|
44
|
-
# Returns an error for already-completed goals so
|
|
45
|
-
#
|
|
46
|
+
# Returns an error for already-completed goals so Melete
|
|
47
|
+
# learns to check status before retrying.
|
|
46
48
|
def complete(goal)
|
|
47
49
|
id = goal.id
|
|
48
50
|
desc = goal.description
|
|
@@ -57,6 +59,7 @@ module AnalyticalBrain
|
|
|
57
59
|
|
|
58
60
|
msg = "Goal completed: #{desc} (id: #{id})"
|
|
59
61
|
msg += " (released #{released} orphaned pins)" if released > 0
|
|
62
|
+
enqueue_goal_message(goal, msg)
|
|
60
63
|
msg
|
|
61
64
|
end
|
|
62
65
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Melete
|
|
4
|
+
module Tools
|
|
5
|
+
# Shared helper for goal tools that enqueue phantom pair messages
|
|
6
|
+
# when Melete creates, updates, or completes a goal.
|
|
7
|
+
#
|
|
8
|
+
# Including classes must set +@main_session+ to the owning {Session}.
|
|
9
|
+
module GoalMessaging
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Enqueues a goal event as a {PendingMessage} on the main session.
|
|
13
|
+
# Promoted to a phantom tool_use/tool_result pair so the main agent
|
|
14
|
+
# sees "I recalled this goal event" in its conversation history.
|
|
15
|
+
#
|
|
16
|
+
# @param goal [Goal] the goal that changed
|
|
17
|
+
# @param confirmation [String] human-readable event description
|
|
18
|
+
# @return [PendingMessage]
|
|
19
|
+
def enqueue_goal_message(goal, confirmation)
|
|
20
|
+
@main_session.pending_messages.create!(
|
|
21
|
+
content: confirmation,
|
|
22
|
+
source_type: "goal",
|
|
23
|
+
source_name: goal.id.to_s,
|
|
24
|
+
message_type: "from_melete_goal"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Reads and activates a workflow on the main session.
|
|
6
|
-
# Returns the full workflow content so
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Returns the full workflow content so Melete can create goals from it.
|
|
7
|
+
# The workflow's content enters the conversation as a phantom
|
|
8
|
+
# tool_use/tool_result pair through the {PendingMessage} promotion flow.
|
|
9
9
|
class ReadWorkflow < ::Tools::Base
|
|
10
10
|
def self.tool_name = "read_workflow"
|
|
11
11
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Renames the main session with an emoji and short descriptive name.
|
|
6
6
|
# Operates on the main session passed through the registry context,
|
|
7
|
-
# not on the phantom
|
|
7
|
+
# not on the phantom Melete session.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# Melete calls this when a conversation's topic becomes
|
|
10
10
|
# clear or shifts significantly enough to warrant a new name.
|
|
11
11
|
class RenameSession < ::Tools::Base
|
|
12
12
|
def self.tool_name = "rename_session"
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Melete
|
|
4
4
|
module Tools
|
|
5
5
|
# Creates a goal on the main session. Root goals represent high-level
|
|
6
6
|
# objectives (semantic episodes); sub-goals are TODO-style steps within
|
|
7
7
|
# a root goal. The two-level hierarchy is enforced by the Goal model.
|
|
8
8
|
class SetGoal < ::Tools::Base
|
|
9
|
+
include GoalMessaging
|
|
10
|
+
|
|
9
11
|
def self.tool_name = "set_goal"
|
|
10
12
|
|
|
11
13
|
def self.description = "Create a goal or sub-goal."
|
|
@@ -40,7 +42,9 @@ module AnalyticalBrain
|
|
|
40
42
|
description: description,
|
|
41
43
|
parent_goal_id: input["parent_goal_id"]
|
|
42
44
|
)
|
|
43
|
-
format_confirmation(goal)
|
|
45
|
+
confirmation = format_confirmation(goal)
|
|
46
|
+
enqueue_goal_message(goal, confirmation)
|
|
47
|
+
confirmation
|
|
44
48
|
rescue ActiveRecord::RecordInvalid => error
|
|
45
49
|
{error: error.record.errors.full_messages.join(", ")}
|
|
46
50
|
end
|