anima-core 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.reek.yml +18 -20
- data/README.md +61 -95
- data/agents/thoughts-analyzer.md +12 -7
- data/anima-core.gemspec +1 -0
- data/app/channels/session_channel.rb +38 -58
- data/app/decorators/agent_message_decorator.rb +7 -2
- data/app/decorators/message_decorator.rb +31 -100
- data/app/decorators/pending_from_melete_decorator.rb +36 -0
- data/app/decorators/pending_from_melete_goal_decorator.rb +13 -0
- data/app/decorators/pending_from_melete_skill_decorator.rb +19 -0
- data/app/decorators/pending_from_melete_workflow_decorator.rb +13 -0
- data/app/decorators/pending_from_mneme_decorator.rb +44 -0
- data/app/decorators/pending_message_decorator.rb +94 -0
- data/app/decorators/pending_subagent_decorator.rb +46 -0
- data/app/decorators/pending_tool_response_decorator.rb +51 -0
- data/app/decorators/pending_user_message_decorator.rb +22 -0
- data/app/decorators/system_message_decorator.rb +5 -0
- data/app/decorators/tool_call_decorator.rb +13 -2
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +7 -2
- data/app/jobs/count_tokens_job.rb +23 -0
- data/app/jobs/drain_job.rb +169 -0
- data/app/jobs/melete_enrichment_job/goal_change_listener.rb +52 -0
- data/app/jobs/melete_enrichment_job.rb +48 -0
- data/app/jobs/mneme_enrichment_job.rb +46 -0
- data/app/jobs/tool_execution_job.rb +87 -0
- data/app/models/concerns/token_estimation.rb +54 -0
- data/app/models/goal.rb +21 -10
- data/app/models/message.rb +47 -36
- data/app/models/pending_message.rb +276 -29
- data/app/models/pinned_message.rb +8 -3
- data/app/models/session.rb +468 -432
- data/app/models/snapshot.rb +11 -21
- data/bin/inspect-cassette +17 -4
- data/config/application.rb +1 -0
- data/config/initializers/event_subscribers.rb +71 -4
- data/config/initializers/inflections.rb +3 -1
- data/db/cable_structure.sql +3 -3
- data/db/migrate/20260407170803_remove_viewport_message_ids_from_sessions.rb +5 -0
- data/db/migrate/20260407180400_remove_mneme_snapshot_pointer_columns_from_sessions.rb +6 -0
- data/db/migrate/20260411120553_add_token_count_to_pinned_messages.rb +5 -0
- data/db/migrate/20260411172926_remove_active_skills_and_workflow_from_sessions.rb +6 -0
- data/db/migrate/20260412110625_replace_processing_with_aasm_state.rb +6 -0
- data/db/migrate/20260418150323_add_kind_and_message_type_to_pending_messages.rb +6 -0
- data/db/migrate/20260419120000_add_drain_fields_to_pending_messages.rb +7 -0
- data/db/migrate/20260419130000_drop_pending_messages_kind_default.rb +5 -0
- data/db/migrate/20260419140000_add_drain_indexes_to_pending_messages.rb +8 -0
- data/db/migrate/20260420100000_add_hud_visibility_to_sessions.rb +15 -0
- data/db/queue_structure.sql +13 -13
- data/db/structure.sql +44 -31
- data/lib/agents/registry.rb +1 -1
- data/lib/anima/settings.rb +7 -33
- data/lib/anima/version.rb +1 -1
- data/lib/events/authentication_required.rb +24 -0
- data/lib/events/bounce_back.rb +4 -4
- data/lib/events/eviction_completed.rb +28 -0
- data/lib/events/goal_created.rb +28 -0
- data/lib/events/goal_updated.rb +32 -0
- data/lib/events/llm_responded.rb +35 -0
- data/lib/events/message_created.rb +27 -0
- data/lib/events/message_updated.rb +25 -0
- data/lib/events/session_state_changed.rb +30 -0
- data/lib/events/skill_activated.rb +28 -0
- data/lib/events/start_melete.rb +36 -0
- data/lib/events/start_mneme.rb +33 -0
- data/lib/events/start_processing.rb +32 -0
- data/lib/events/subagent_evicted.rb +31 -0
- data/lib/events/subscribers/active_state_broadcaster.rb +27 -0
- data/lib/events/subscribers/authentication_broadcaster.rb +34 -0
- data/lib/events/subscribers/drain_kickoff.rb +20 -0
- data/lib/events/subscribers/eviction_broadcaster.rb +26 -0
- data/lib/events/subscribers/llm_response_handler.rb +111 -0
- data/lib/events/subscribers/melete_kickoff.rb +24 -0
- data/lib/events/subscribers/message_broadcaster.rb +34 -0
- data/lib/events/subscribers/mneme_kickoff.rb +24 -0
- data/lib/events/subscribers/mneme_scheduler.rb +21 -0
- data/lib/events/subscribers/persister.rb +6 -8
- data/lib/events/subscribers/session_state_broadcaster.rb +33 -0
- data/lib/events/subscribers/subagent_message_router.rb +26 -29
- data/lib/events/subscribers/subagent_visibility_broadcaster.rb +33 -0
- data/lib/events/subscribers/tool_response_creator.rb +33 -0
- data/lib/events/subscribers/transient_broadcaster.rb +1 -1
- data/lib/events/tool_executed.rb +34 -0
- data/lib/events/workflow_activated.rb +27 -0
- data/lib/llm/client.rb +41 -201
- data/lib/mcp/client_manager.rb +41 -46
- data/lib/mcp/stdio_transport.rb +9 -5
- data/lib/{analytical_brain → melete}/runner.rb +63 -68
- data/lib/{analytical_brain → melete}/tools/activate_skill.rb +1 -1
- data/lib/{analytical_brain → melete}/tools/assign_nickname.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/everything_is_ready.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/finish_goal.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/goal_messaging.rb +4 -3
- data/lib/{analytical_brain → melete}/tools/read_workflow.rb +2 -2
- data/lib/{analytical_brain → melete}/tools/rename_session.rb +3 -3
- data/lib/{analytical_brain → melete}/tools/set_goal.rb +1 -1
- data/lib/{analytical_brain → melete}/tools/update_goal.rb +4 -4
- data/lib/{analytical_brain.rb → melete.rb} +6 -3
- data/lib/mneme/base_runner.rb +121 -0
- data/lib/mneme/l2_runner.rb +14 -20
- data/lib/mneme/recall_runner.rb +132 -0
- data/lib/mneme/runner.rb +118 -171
- data/lib/mneme/search.rb +104 -62
- data/lib/mneme/tools/nothing_to_surface.rb +25 -0
- data/lib/mneme/tools/save_snapshot.rb +2 -10
- data/lib/mneme/tools/surface_memory.rb +89 -0
- data/lib/mneme.rb +11 -5
- data/lib/shell_session.rb +287 -612
- data/lib/skills/definition.rb +2 -2
- data/lib/skills/registry.rb +1 -1
- data/lib/tools/base.rb +16 -0
- data/lib/tools/bash.rb +25 -57
- data/lib/tools/edit.rb +2 -0
- data/lib/tools/read.rb +2 -0
- data/lib/tools/registry.rb +79 -3
- data/lib/tools/{recall.rb → search_messages.rb} +19 -21
- data/lib/tools/spawn_specialist.rb +16 -10
- data/lib/tools/spawn_subagent.rb +20 -14
- data/lib/tools/subagent_prompts.rb +4 -4
- data/lib/tools/think.rb +1 -1
- data/lib/tools/{remember.rb → view_messages.rb} +10 -10
- data/lib/tools/write.rb +2 -0
- data/lib/tui/app.rb +5 -4
- data/lib/tui/braille_spinner.rb +7 -7
- data/lib/tui/decorators/base_decorator.rb +24 -3
- data/lib/tui/message_store.rb +93 -44
- data/lib/tui/screens/chat.rb +94 -20
- data/lib/tui/settings.rb +9 -2
- data/lib/workflows/definition.rb +3 -3
- data/lib/workflows/registry.rb +1 -1
- data/skills/github.md +38 -0
- data/templates/config.toml +4 -23
- data/workflows/review_pr.md +18 -14
- metadata +86 -28
- data/app/jobs/agent_request_job.rb +0 -199
- data/app/jobs/analytical_brain_job.rb +0 -33
- data/app/jobs/count_message_tokens_job.rb +0 -39
- data/app/jobs/passive_recall_job.rb +0 -24
- data/app/models/concerns/message/broadcasting.rb +0 -86
- data/lib/agent_loop.rb +0 -215
- data/lib/analytical_brain/tools/deactivate_skill.rb +0 -40
- data/lib/analytical_brain/tools/deactivate_workflow.rb +0 -35
- data/lib/events/agent_message.rb +0 -25
- data/lib/events/subscribers/message_collector.rb +0 -64
- data/lib/events/tool_call.rb +0 -31
- data/lib/events/tool_response.rb +0 -33
- data/lib/mneme/compressed_viewport.rb +0 -204
- data/lib/mneme/passive_recall.rb +0 -138
data/lib/skills/definition.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Skills
|
|
|
10
10
|
# content injected into the main agent's system prompt when active.
|
|
11
11
|
#
|
|
12
12
|
# Skills are passive knowledge — they describe WHAT you know, not
|
|
13
|
-
# WHAT to do.
|
|
13
|
+
# WHAT to do. Melete activates/deactivates them based
|
|
14
14
|
# on conversation context.
|
|
15
15
|
#
|
|
16
16
|
# @example Skill file format
|
|
@@ -25,7 +25,7 @@ module Skills
|
|
|
25
25
|
# @return [String] unique skill identifier used in activate_skill(name: "...")
|
|
26
26
|
attr_reader :name
|
|
27
27
|
|
|
28
|
-
# @return [String] description shown to
|
|
28
|
+
# @return [String] description shown to Melete for relevance matching
|
|
29
29
|
attr_reader :description
|
|
30
30
|
|
|
31
31
|
# @return [String] knowledge content (Markdown body) injected into system prompt
|
data/lib/skills/registry.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Skills
|
|
|
69
69
|
@skills[name]
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
# Skill names and descriptions for inclusion in
|
|
72
|
+
# Skill names and descriptions for inclusion in Melete's context.
|
|
73
73
|
#
|
|
74
74
|
# @return [Hash{String => String}] name => description
|
|
75
75
|
def catalog
|
data/lib/tools/base.rb
CHANGED
|
@@ -50,6 +50,22 @@ module Tools
|
|
|
50
50
|
def truncation_threshold
|
|
51
51
|
Anima::Settings.max_tool_response_chars
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# One-line entry rendered into the system prompt's "## Available Tools"
|
|
55
|
+
# menu. Tools that return +nil+ are omitted from the menu.
|
|
56
|
+
#
|
|
57
|
+
# @return [String, nil] short capability statement, or nil to skip
|
|
58
|
+
def prompt_snippet
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Cross-tool behavioral guidelines merged into the system prompt's
|
|
63
|
+
# "## Tool Guidelines" section. Each entry becomes a Markdown bullet.
|
|
64
|
+
#
|
|
65
|
+
# @return [Array<String>] guideline lines, or empty array to skip
|
|
66
|
+
def prompt_guidelines
|
|
67
|
+
[]
|
|
68
|
+
end
|
|
53
69
|
end
|
|
54
70
|
|
|
55
71
|
# Subclasses whose schema depends on runtime context (e.g. session state,
|
data/lib/tools/bash.rb
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
module Tools
|
|
4
4
|
# Executes bash commands in a persistent {ShellSession}. Commands share
|
|
5
5
|
# working directory, environment variables, and shell history within a
|
|
6
|
-
# conversation. Output is
|
|
7
|
-
#
|
|
6
|
+
# conversation. Output is the rendered terminal text exactly as a human
|
|
7
|
+
# would see it — including the prompt, which doubles as live cwd/branch
|
|
8
|
+
# telemetry for the agent.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
# -
|
|
11
|
-
# -
|
|
10
|
+
# Two input shapes:
|
|
11
|
+
# - +command+ (string) — one command, one result.
|
|
12
|
+
# - +commands+ (array) — runs each command in order in the same shell;
|
|
13
|
+
# all run regardless of failures (the agent reads merged output and
|
|
14
|
+
# decides what to do). Use shell chaining (+&&+) inside a single
|
|
15
|
+
# command if you need fail-fast.
|
|
12
16
|
#
|
|
13
17
|
# @see ShellSession#run
|
|
14
18
|
class Bash < Base
|
|
@@ -16,6 +20,8 @@ module Tools
|
|
|
16
20
|
|
|
17
21
|
def self.description = "Execute shell commands. Working directory and environment persist between calls."
|
|
18
22
|
|
|
23
|
+
def self.prompt_snippet = "Run shell commands."
|
|
24
|
+
|
|
19
25
|
def self.input_schema
|
|
20
26
|
{
|
|
21
27
|
type: "object",
|
|
@@ -26,12 +32,7 @@ module Tools
|
|
|
26
32
|
commands: {
|
|
27
33
|
type: "array",
|
|
28
34
|
items: {type: "string"},
|
|
29
|
-
description: "Each command gets its own timeout and result."
|
|
30
|
-
},
|
|
31
|
-
mode: {
|
|
32
|
-
type: "string",
|
|
33
|
-
enum: ["sequential", "parallel"],
|
|
34
|
-
description: "sequential (default) stops on first failure."
|
|
35
|
+
description: "Each command gets its own timeout and result. All commands run regardless of failures — use a single command with shell chaining if you need fail-fast."
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -47,7 +48,7 @@ module Tools
|
|
|
47
48
|
# @param input [Hash<String, Object>] string-keyed hash from the Anthropic API.
|
|
48
49
|
# Supports optional "timeout" key (seconds) to override the global
|
|
49
50
|
# command_timeout setting for long-running operations.
|
|
50
|
-
# @return [String]
|
|
51
|
+
# @return [String] rendered terminal output
|
|
51
52
|
# @return [Hash] with :error key on failure
|
|
52
53
|
def execute(input)
|
|
53
54
|
timeout = input["timeout"]
|
|
@@ -57,7 +58,7 @@ module Tools
|
|
|
57
58
|
if has_command && has_commands
|
|
58
59
|
{error: "Provide either 'command' or 'commands', not both"}
|
|
59
60
|
elsif has_commands
|
|
60
|
-
execute_batch(input["commands"],
|
|
61
|
+
execute_batch(input["commands"], timeout: timeout)
|
|
61
62
|
elsif has_command
|
|
62
63
|
execute_single(input["command"], timeout: timeout)
|
|
63
64
|
else
|
|
@@ -77,30 +78,26 @@ module Tools
|
|
|
77
78
|
return format_interrupted(result) if result[:interrupted]
|
|
78
79
|
return result if result.key?(:error)
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
append_env_summary(output, result[:env_summary])
|
|
81
|
+
result[:output]
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Executes an array of commands
|
|
85
|
-
#
|
|
86
|
-
#
|
|
84
|
+
# Executes an array of commands sequentially through the shared
|
|
85
|
+
# shell. Continues past errors — the LLM reads the merged output
|
|
86
|
+
# and decides what to do. The only short-circuit is a user interrupt,
|
|
87
|
+
# which skips the remaining commands.
|
|
87
88
|
#
|
|
88
89
|
# @param commands [Array<String>] commands to execute
|
|
89
|
-
# @param mode [String] "sequential" (stop on first failure) or "parallel" (run all)
|
|
90
90
|
# @param timeout [Integer, nil] per-command timeout override
|
|
91
91
|
# @return [String] combined results with per-command headers
|
|
92
92
|
# @return [Hash] with :error key if commands array is invalid
|
|
93
|
-
def execute_batch(commands,
|
|
93
|
+
def execute_batch(commands, timeout: nil)
|
|
94
94
|
return {error: "Commands array cannot be empty"} unless commands.is_a?(Array) && commands.any?
|
|
95
95
|
|
|
96
96
|
checker = interrupt_checker
|
|
97
97
|
total = commands.size
|
|
98
98
|
results = []
|
|
99
|
-
failed = false
|
|
100
99
|
interrupted = false
|
|
101
100
|
|
|
102
|
-
last_env_summary = nil
|
|
103
|
-
|
|
104
101
|
commands.each_with_index do |command, index|
|
|
105
102
|
position = "[#{index + 1}/#{total}]"
|
|
106
103
|
|
|
@@ -109,11 +106,6 @@ module Tools
|
|
|
109
106
|
next
|
|
110
107
|
end
|
|
111
108
|
|
|
112
|
-
if failed && mode == "sequential"
|
|
113
|
-
results << "#{position} $ #{command}\n(skipped)"
|
|
114
|
-
next
|
|
115
|
-
end
|
|
116
|
-
|
|
117
109
|
command = command.to_s
|
|
118
110
|
if command.strip.empty?
|
|
119
111
|
results << "#{position} $ (blank)\n(skipped — blank command)"
|
|
@@ -127,47 +119,23 @@ module Tools
|
|
|
127
119
|
interrupted = true
|
|
128
120
|
elsif result.key?(:error)
|
|
129
121
|
results << "#{position} $ #{command}\n#{result[:error]}"
|
|
130
|
-
failed = true
|
|
131
122
|
else
|
|
132
|
-
|
|
133
|
-
output = format_result(result[:stdout], result[:stderr], exit_code)
|
|
134
|
-
results << "#{position} $ #{command}\n#{output}"
|
|
135
|
-
last_env_summary = result[:env_summary]
|
|
136
|
-
failed = true if exit_code != 0
|
|
123
|
+
results << "#{position} $ #{command}\n#{result[:output]}"
|
|
137
124
|
end
|
|
138
125
|
end
|
|
139
126
|
|
|
140
|
-
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Appends environment summary to tool output when present.
|
|
144
|
-
#
|
|
145
|
-
# @param output [String] formatted tool response
|
|
146
|
-
# @param env_summary [String, nil] natural-language environment change summary
|
|
147
|
-
# @return [String] output with env summary appended
|
|
148
|
-
def append_env_summary(output, env_summary)
|
|
149
|
-
env_summary ? "#{output}\n\n#{env_summary}" : output
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def format_result(stdout, stderr, exit_code)
|
|
153
|
-
parts = []
|
|
154
|
-
parts << "stdout:\n#{stdout}" unless stdout.empty?
|
|
155
|
-
parts << "stderr:\n#{stderr}" unless stderr.empty?
|
|
156
|
-
parts << "exit_code: #{exit_code}"
|
|
157
|
-
parts.join("\n\n")
|
|
127
|
+
results.join("\n\n")
|
|
158
128
|
end
|
|
159
129
|
|
|
160
130
|
# Formats the result of an interrupted command for the LLM.
|
|
161
131
|
# Includes partial output captured before the interrupt.
|
|
162
132
|
#
|
|
163
|
-
# @param result [Hash] ShellSession result with :
|
|
133
|
+
# @param result [Hash] ShellSession result with :output key
|
|
164
134
|
# @return [String] formatted message for the LLM
|
|
165
135
|
def format_interrupted(result)
|
|
166
|
-
|
|
167
|
-
stderr = result[:stderr].to_s
|
|
136
|
+
output = result[:output].to_s
|
|
168
137
|
parts = [LLM::Client::INTERRUPT_MESSAGE]
|
|
169
|
-
parts << "Partial
|
|
170
|
-
parts << "stderr:\n#{stderr}" unless stderr.empty?
|
|
138
|
+
parts << "Partial output:\n#{output}" unless output.empty?
|
|
171
139
|
parts.join("\n\n")
|
|
172
140
|
end
|
|
173
141
|
|
data/lib/tools/edit.rb
CHANGED
data/lib/tools/read.rb
CHANGED
data/lib/tools/registry.rb
CHANGED
|
@@ -14,6 +14,76 @@ module Tools
|
|
|
14
14
|
# registry.register(Tools::Bash)
|
|
15
15
|
# registry.execute("bash", {"command" => "ls"})
|
|
16
16
|
class Registry
|
|
17
|
+
# Standard tools available to every session unless filtered out by
|
|
18
|
+
# {Session#granted_tools}.
|
|
19
|
+
STANDARD_TOOLS = [Tools::Bash, Tools::Read, Tools::Write, Tools::Edit, Tools::WebGet, Tools::Think, Tools::ViewMessages, Tools::SearchMessages].freeze
|
|
20
|
+
|
|
21
|
+
# Tools that bypass {Session#granted_tools} filtering — the agent's
|
|
22
|
+
# reasoning depends on them regardless of task scope.
|
|
23
|
+
ALWAYS_GRANTED_TOOLS = [Tools::Think].freeze
|
|
24
|
+
|
|
25
|
+
# Name-to-class mapping for granted-tools filtering.
|
|
26
|
+
STANDARD_TOOLS_BY_NAME = STANDARD_TOOLS.index_by(&:tool_name).freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Builds a registry appropriate for the given session: standard tools
|
|
30
|
+
# filtered through {Session#granted_tools}, plus spawn tools for main
|
|
31
|
+
# sessions or +mark_goal_completed+ for sub-agents, plus any tools
|
|
32
|
+
# exposed by configured MCP servers.
|
|
33
|
+
#
|
|
34
|
+
# MCP registration warnings are emitted as system messages so both the
|
|
35
|
+
# user (in verbose mode) and the LLM see them.
|
|
36
|
+
#
|
|
37
|
+
# @param session [Session] the session requesting tools
|
|
38
|
+
# @param shell_session [ShellSession] persistent PTY for Bash-family tools
|
|
39
|
+
# @return [Registry] configured registry
|
|
40
|
+
def build(session:, shell_session:)
|
|
41
|
+
registry = new(context: {shell_session: shell_session, session: session})
|
|
42
|
+
|
|
43
|
+
tool_classes_for(session).each { |tool| registry.register(tool) }
|
|
44
|
+
|
|
45
|
+
Mcp::ClientManager.shared.register_tools(registry).each do |message|
|
|
46
|
+
Events::Bus.emit(Events::SystemMessage.new(content: message, session_id: session.id))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
registry
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Resolves the ordered tool-class list for a session: granted standard
|
|
53
|
+
# tools (or all of them when +granted_tools+ is nil), plus spawn tools
|
|
54
|
+
# for main sessions or +mark_goal_completed+ for sub-agents. MCP tools
|
|
55
|
+
# are excluded — they are dynamic and registered separately by
|
|
56
|
+
# {.build}. Single source of truth for {.build}, {Session#tool_schemas},
|
|
57
|
+
# and the system-prompt section assemblers.
|
|
58
|
+
#
|
|
59
|
+
# @param session [Session]
|
|
60
|
+
# @return [Array<Class<Tools::Base>>]
|
|
61
|
+
def tool_classes_for(session)
|
|
62
|
+
tools = granted_standard_tools(session).dup
|
|
63
|
+
|
|
64
|
+
if session.sub_agent?
|
|
65
|
+
tools.push(Tools::MarkGoalCompleted)
|
|
66
|
+
else
|
|
67
|
+
tools.push(Tools::SpawnSubagent, Tools::SpawnSpecialist, Tools::OpenIssue)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
tools
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Filters {STANDARD_TOOLS} through the session's granted list.
|
|
76
|
+
# Always includes {ALWAYS_GRANTED_TOOLS} so the agent retains core
|
|
77
|
+
# reasoning tools regardless of task scope.
|
|
78
|
+
def granted_standard_tools(session)
|
|
79
|
+
granted = session.granted_tools
|
|
80
|
+
return STANDARD_TOOLS unless granted
|
|
81
|
+
|
|
82
|
+
explicitly_granted = granted.filter_map { |name| STANDARD_TOOLS_BY_NAME[name] }
|
|
83
|
+
(ALWAYS_GRANTED_TOOLS + explicitly_granted).uniq
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
17
87
|
# @return [Hash{String => Class, Object}] registered tools keyed by name
|
|
18
88
|
attr_reader :tools
|
|
19
89
|
|
|
@@ -49,16 +119,22 @@ module Tools
|
|
|
49
119
|
end
|
|
50
120
|
|
|
51
121
|
# Execute a tool by name. Classes are instantiated with the registry's
|
|
52
|
-
# context; instances are called directly.
|
|
122
|
+
# context; instances are called directly. The enclosing +tool_use_id+
|
|
123
|
+
# is merged into the context when provided so tools that need to
|
|
124
|
+
# reference their own invoking tool_call (e.g. {Tools::SpawnSubagent}
|
|
125
|
+
# persisting +spawn_tool_use_id+ on the child session) can read it via
|
|
126
|
+
# a named kwarg in their initializer.
|
|
53
127
|
#
|
|
54
128
|
# @param name [String] registered tool name
|
|
55
129
|
# @param input [Hash] tool input parameters (may include "timeout" for
|
|
56
130
|
# tools that support per-call timeout overrides)
|
|
131
|
+
# @param tool_use_id [String, nil] the invoking tool_call's pairing id
|
|
57
132
|
# @return [String, Hash] tool execution result
|
|
58
133
|
# @raise [UnknownToolError] if no tool is registered with the given name
|
|
59
|
-
def execute(name, input)
|
|
134
|
+
def execute(name, input, tool_use_id: nil)
|
|
60
135
|
tool = @tools.fetch(name) { raise UnknownToolError, "Unknown tool: #{name}" }
|
|
61
|
-
|
|
136
|
+
context = tool_use_id ? @context.merge(tool_use_id: tool_use_id) : @context
|
|
137
|
+
instance = tool.is_a?(Class) ? tool.new(**context) : tool
|
|
62
138
|
instance.execute(input)
|
|
63
139
|
end
|
|
64
140
|
|
|
@@ -1,51 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Tools
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Keyword search across long-term memory — every message Anima has
|
|
5
|
+
# ever seen, across every session, *except* what the agent is already
|
|
6
|
+
# looking at right now. Results sit below her current viewport; anything
|
|
7
|
+
# still in front of her is excluded, so every slot the search returns is
|
|
8
|
+
# new information.
|
|
6
9
|
#
|
|
7
10
|
# Two-step memory workflow:
|
|
8
|
-
# 1. `
|
|
9
|
-
# 2. `
|
|
11
|
+
# 1. `search_messages(query: "auth flow")` → discovers relevant messages
|
|
12
|
+
# 2. `view_messages(message_id: 42)` → fractal zoom into full context
|
|
10
13
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
14
|
+
# Same FTS5 engine Mneme uses for passive recall — this variant fires
|
|
15
|
+
# on demand when Aoide reaches for a memory herself.
|
|
13
16
|
#
|
|
14
|
-
# @example
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# recall(query: "OAuth config", session_only: true)
|
|
19
|
-
class Recall < Base
|
|
20
|
-
def self.tool_name = "recall"
|
|
17
|
+
# @example
|
|
18
|
+
# search_messages(query: "authentication flow")
|
|
19
|
+
class SearchMessages < Base
|
|
20
|
+
def self.tool_name = "search_messages"
|
|
21
21
|
|
|
22
|
-
def self.description = "
|
|
22
|
+
def self.description = "Search long-term memory (past conversations outside your current viewport) by keyword. Returns ranked message snippets with IDs — pass any ID to view_messages to see the full context around it."
|
|
23
23
|
|
|
24
24
|
def self.input_schema
|
|
25
25
|
{
|
|
26
26
|
type: "object",
|
|
27
27
|
properties: {
|
|
28
|
-
query: {type: "string"}
|
|
29
|
-
session_only: {type: "boolean", description: "Default: all sessions"}
|
|
28
|
+
query: {type: "string"}
|
|
30
29
|
},
|
|
31
30
|
required: ["query"]
|
|
32
31
|
}
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
# @param session [Session] the
|
|
34
|
+
# @param session [Session] the calling session — drives viewport exclusion
|
|
36
35
|
def initialize(session:, **)
|
|
37
36
|
@session = session
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
# @param input [Hash] with "query"
|
|
39
|
+
# @param input [Hash] with "query"
|
|
41
40
|
# @return [String] formatted search results with message IDs
|
|
42
41
|
# @return [Hash] with :error key when query is blank
|
|
43
42
|
def execute(input)
|
|
44
43
|
query = input["query"].to_s.strip
|
|
45
44
|
return {error: "Query cannot be blank"} if query.empty?
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
results = Mneme::Search.query(query, session_id: session_id)
|
|
46
|
+
results = Mneme::Search.query(query, caller_session: @session)
|
|
49
47
|
|
|
50
48
|
return "No results found for \"#{query}\"." if results.empty?
|
|
51
49
|
|
|
@@ -55,7 +53,7 @@ module Tools
|
|
|
55
53
|
private
|
|
56
54
|
|
|
57
55
|
# Formats results as token-efficient, LLM-readable output.
|
|
58
|
-
# Each result includes message_id for drill-down via
|
|
56
|
+
# Each result includes message_id for drill-down via view_messages.
|
|
59
57
|
#
|
|
60
58
|
# @param query [String] the original search query
|
|
61
59
|
# @param results [Array<Mneme::Search::Result>] ranked search results
|
|
@@ -5,7 +5,7 @@ module Tools
|
|
|
5
5
|
# The specialist has a predefined system prompt and tool set defined
|
|
6
6
|
# in its Markdown definition file under agents/.
|
|
7
7
|
#
|
|
8
|
-
# Nickname assignment is handled by the {
|
|
8
|
+
# Nickname assignment is handled by the {Melete::Runner} which
|
|
9
9
|
# runs synchronously at spawn time, generating a unique nickname based
|
|
10
10
|
# on the task — same as generic sub-agents.
|
|
11
11
|
#
|
|
@@ -58,17 +58,22 @@ module Tools
|
|
|
58
58
|
private_class_method :name_property
|
|
59
59
|
|
|
60
60
|
# @param session [Session] the parent session spawning the specialist
|
|
61
|
-
# @param shell_session [ShellSession] the parent's persistent shell (for CWD inheritance)
|
|
62
61
|
# @param agent_registry [Agents::Registry, nil] injectable for testing
|
|
63
|
-
|
|
62
|
+
# @param tool_use_id [String, nil] the invoking +spawn_specialist+ tool_call's
|
|
63
|
+
# pairing id, captured so the spawn pair can later be located by the
|
|
64
|
+
# HUD visibility sweep in {Mneme::Runner}
|
|
65
|
+
def initialize(session:, agent_registry: nil, tool_use_id: nil, **)
|
|
64
66
|
@session = session
|
|
65
|
-
@shell_session = shell_session
|
|
66
67
|
@agent_registry = agent_registry || Agents::Registry.instance
|
|
68
|
+
@tool_use_id = tool_use_id
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
# Creates a child session with the specialist's predefined prompt and
|
|
70
|
-
#
|
|
71
|
-
#
|
|
71
|
+
# Creates a child session with the specialist's predefined prompt and
|
|
72
|
+
# tools, pins the task as a Goal, and enqueues the task as the
|
|
73
|
+
# child's first user_message PendingMessage — which kicks the
|
|
74
|
+
# standard inbound pipeline (Melete → (Mneme) → StartProcessing →
|
|
75
|
+
# DrainJob) so the specialist self-starts the same way a human-typed
|
|
76
|
+
# message would. Returns immediately after Melete completes.
|
|
72
77
|
#
|
|
73
78
|
# @param input [Hash<String, Object>] with "name" and "task"
|
|
74
79
|
# @return [String] confirmation with child session ID
|
|
@@ -97,12 +102,13 @@ module Tools
|
|
|
97
102
|
parent_session_id: @session.id,
|
|
98
103
|
prompt: build_prompt(definition),
|
|
99
104
|
granted_tools: definition.tools,
|
|
100
|
-
|
|
105
|
+
spawn_tool_use_id: @tool_use_id,
|
|
106
|
+
initial_cwd: ShellSession.cwd_via_tmux(@session.id) || @session.initial_cwd
|
|
101
107
|
)
|
|
102
108
|
create_goal_with_pinned_task(child, task)
|
|
103
|
-
|
|
109
|
+
assign_nickname_via_melete(child)
|
|
104
110
|
child.broadcast_children_update_to_parent
|
|
105
|
-
|
|
111
|
+
child.enqueue_user_message(task)
|
|
106
112
|
child
|
|
107
113
|
end
|
|
108
114
|
|
data/lib/tools/spawn_subagent.rb
CHANGED
|
@@ -4,11 +4,11 @@ module Tools
|
|
|
4
4
|
# Spawns a generic child session that works on a task autonomously.
|
|
5
5
|
# The sub-agent starts clean — no parent conversation history — with
|
|
6
6
|
# only a system prompt, a Goal, and the task as its first user message.
|
|
7
|
-
# Runs via {
|
|
7
|
+
# Runs via {DrainJob} and communicates with the parent through
|
|
8
8
|
# natural text messages routed by {Events::Subscribers::SubagentMessageRouter}.
|
|
9
9
|
#
|
|
10
|
-
# Nickname assignment is handled by the {
|
|
11
|
-
# runs synchronously at spawn time — the same
|
|
10
|
+
# Nickname assignment is handled by the {Melete::Runner} which
|
|
11
|
+
# runs synchronously at spawn time — the same muse that manages skills,
|
|
12
12
|
# goals, and workflows for the main session.
|
|
13
13
|
#
|
|
14
14
|
# For named specialists with predefined prompts and tools, see {SpawnSpecialist}.
|
|
@@ -36,7 +36,7 @@ module Tools
|
|
|
36
36
|
items: {type: "string"},
|
|
37
37
|
description: "Tool names to grant the sub-agent. " \
|
|
38
38
|
"Omit for all standard tools. Empty array for pure reasoning. " \
|
|
39
|
-
"Valid tools: #{
|
|
39
|
+
"Valid tools: #{Tools::Registry::STANDARD_TOOLS_BY_NAME.keys.join(", ")}"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
required: %w[task]
|
|
@@ -44,16 +44,21 @@ module Tools
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# @param session [Session] the parent session spawning the sub-agent
|
|
47
|
-
# @param
|
|
48
|
-
|
|
47
|
+
# @param tool_use_id [String, nil] the invoking +spawn_subagent+ tool_call's
|
|
48
|
+
# pairing id, captured so the spawn pair can later be located by the
|
|
49
|
+
# HUD visibility sweep in {Mneme::Runner}
|
|
50
|
+
def initialize(session:, tool_use_id: nil, **)
|
|
49
51
|
@session = session
|
|
50
|
-
@
|
|
52
|
+
@tool_use_id = tool_use_id
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
# Creates a child session with a clean context (no parent history),
|
|
54
|
-
# runs
|
|
55
|
-
#
|
|
56
|
-
#
|
|
56
|
+
# runs Melete to assign a nickname, pins the task as a Goal, and
|
|
57
|
+
# enqueues the task as the child's first user_message PendingMessage —
|
|
58
|
+
# which kicks the standard inbound pipeline (Melete → (Mneme) →
|
|
59
|
+
# StartProcessing → DrainJob) so the sub-agent self-starts the same
|
|
60
|
+
# way a human-typed message would. Returns immediately after Melete
|
|
61
|
+
# completes (blocking for ~200ms).
|
|
57
62
|
#
|
|
58
63
|
# @param input [Hash<String, Object>] with "task" and optional "tools"
|
|
59
64
|
# @return [String] confirmation with child session ID and @nickname
|
|
@@ -82,12 +87,13 @@ module Tools
|
|
|
82
87
|
parent_session_id: @session.id,
|
|
83
88
|
prompt: GENERIC_PROMPT,
|
|
84
89
|
granted_tools: granted_tools,
|
|
85
|
-
|
|
90
|
+
spawn_tool_use_id: @tool_use_id,
|
|
91
|
+
initial_cwd: ShellSession.cwd_via_tmux(@session.id) || @session.initial_cwd
|
|
86
92
|
)
|
|
87
93
|
create_goal_with_pinned_task(child, task)
|
|
88
|
-
|
|
94
|
+
assign_nickname_via_melete(child)
|
|
89
95
|
child.broadcast_children_update_to_parent
|
|
90
|
-
|
|
96
|
+
child.enqueue_user_message(task)
|
|
91
97
|
child
|
|
92
98
|
end
|
|
93
99
|
|
|
@@ -109,7 +115,7 @@ module Tools
|
|
|
109
115
|
return nil unless tools
|
|
110
116
|
return {error: "tools must be an array"} unless tools.is_a?(Array)
|
|
111
117
|
|
|
112
|
-
unknown = tools -
|
|
118
|
+
unknown = tools - Tools::Registry::STANDARD_TOOLS_BY_NAME.keys
|
|
113
119
|
return {error: "Unknown tool: #{unknown.first}"} if unknown.any?
|
|
114
120
|
|
|
115
121
|
nil
|
|
@@ -30,13 +30,13 @@ module Tools
|
|
|
30
30
|
GoalPinnedMessage.create!(goal: goal, pinned_message: pin)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# Runs
|
|
33
|
+
# Runs Melete synchronously to assign a nickname,
|
|
34
34
|
# then prepends identity context to the stored prompt.
|
|
35
35
|
# Falls back to a sequential "agent-N" name on any failure.
|
|
36
36
|
# Identity injection runs in +ensure+ so it applies to both
|
|
37
|
-
#
|
|
38
|
-
def
|
|
39
|
-
|
|
37
|
+
# Melete-assigned and fallback nicknames.
|
|
38
|
+
def assign_nickname_via_melete(child)
|
|
39
|
+
Melete::Runner.new(child).call
|
|
40
40
|
child.reload
|
|
41
41
|
rescue => error
|
|
42
42
|
Rails.logger.warn("Sub-agent nickname assignment failed: #{error.message}")
|
data/lib/tools/think.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Tools
|
|
|
5
5
|
# pause between tool calls where the agent can organize thoughts, plan
|
|
6
6
|
# next steps, or make decisions without interrupting the user.
|
|
7
7
|
#
|
|
8
|
-
# Think events bridge the gap between
|
|
8
|
+
# Think events bridge the gap between Melete (subconscious
|
|
9
9
|
# background processing) and speech (user-facing messages). Without this
|
|
10
10
|
# tool, reasoning leaks into tool arguments as comments.
|
|
11
11
|
#
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Tools
|
|
4
|
-
# Fractal-resolution
|
|
5
|
-
#
|
|
6
|
-
# at the edges — sharp fovea, blurry periphery.
|
|
4
|
+
# Fractal-resolution window into long-term memory. Given a message_id,
|
|
5
|
+
# returns the surrounding conversation with full detail at the center
|
|
6
|
+
# and compressed snapshots at the edges — sharp fovea, blurry periphery.
|
|
7
7
|
#
|
|
8
8
|
# Output structure:
|
|
9
9
|
# [Previous snapshots — compressed context before]
|
|
10
10
|
# [Messages N-M — full detail, tool_responses compressed to checkmarks]
|
|
11
11
|
# [Following snapshots — compressed context after]
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# Aoide discovers target message IDs via {SearchMessages} and drills
|
|
14
|
+
# down here to recover the full context around any moment.
|
|
15
15
|
#
|
|
16
16
|
# @example
|
|
17
|
-
#
|
|
18
|
-
class
|
|
17
|
+
# view_messages(message_id: 42)
|
|
18
|
+
class ViewMessages < Base
|
|
19
19
|
# Messages around the target to include at full resolution.
|
|
20
20
|
# ±10 messages provides sharp foveal detail while keeping output readable.
|
|
21
21
|
CONTEXT_WINDOW = 20
|
|
@@ -26,9 +26,9 @@ module Tools
|
|
|
26
26
|
"system_message" => "System"
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
|
-
def self.tool_name = "
|
|
29
|
+
def self.tool_name = "view_messages"
|
|
30
30
|
|
|
31
|
-
def self.description = "
|
|
31
|
+
def self.description = "View the full conversation around a message in long-term memory. Pass a message_id — typically one returned by search_messages — to see the surrounding exchange with compressed snapshots at the edges."
|
|
32
32
|
|
|
33
33
|
def self.input_schema
|
|
34
34
|
{
|
|
@@ -112,7 +112,7 @@ module Tools
|
|
|
112
112
|
# @return [Array<Message>] chronologically ordered
|
|
113
113
|
def fetch_center_messages(target, target_session)
|
|
114
114
|
half = CONTEXT_WINDOW / 2
|
|
115
|
-
scope = target_session.messages
|
|
115
|
+
scope = target_session.messages
|
|
116
116
|
target_id = target.id
|
|
117
117
|
|
|
118
118
|
before = scope.where("id <= ?", target_id).reorder(id: :desc).limit(half + 1).to_a.reverse
|