anima-core 1.1.3 → 1.2.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 +2 -0
- data/agents/codebase-analyzer.md +1 -1
- data/agents/codebase-pattern-finder.md +1 -1
- data/agents/documentation-researcher.md +1 -1
- data/agents/thoughts-analyzer.md +1 -1
- data/agents/web-search-researcher.md +1 -1
- data/app/channels/session_channel.rb +44 -43
- data/app/decorators/agent_message_decorator.rb +2 -2
- data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
- data/app/decorators/system_message_decorator.rb +2 -2
- data/app/decorators/tool_call_decorator.rb +2 -2
- data/app/decorators/tool_decorator.rb +4 -4
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +3 -3
- data/app/decorators/web_get_tool_decorator.rb +41 -9
- data/app/jobs/agent_request_job.rb +20 -20
- data/app/jobs/count_message_tokens_job.rb +39 -0
- data/app/jobs/passive_recall_job.rb +4 -4
- data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
- data/app/models/goal.rb +4 -4
- data/app/models/goal_pinned_message.rb +11 -0
- data/app/models/{event.rb → message.rb} +42 -39
- data/app/models/pinned_message.rb +41 -0
- data/app/models/session.rb +206 -198
- data/app/models/snapshot.rb +25 -25
- data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
- data/lib/agent_loop.rb +6 -6
- data/lib/analytical_brain/runner.rb +35 -35
- data/lib/analytical_brain/tools/activate_skill.rb +5 -9
- data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
- data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
- data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
- data/lib/analytical_brain/tools/finish_goal.rb +5 -8
- data/lib/analytical_brain/tools/read_workflow.rb +5 -9
- data/lib/analytical_brain/tools/rename_session.rb +3 -10
- data/lib/analytical_brain/tools/set_goal.rb +3 -7
- data/lib/analytical_brain/tools/update_goal.rb +3 -7
- data/lib/anima/settings.rb +15 -4
- data/lib/anima/version.rb +1 -1
- data/lib/events/bounce_back.rb +7 -7
- data/lib/events/subscribers/persister.rb +7 -7
- data/lib/events/subscribers/transient_broadcaster.rb +2 -2
- data/lib/mneme/compressed_viewport.rb +57 -57
- data/lib/mneme/l2_runner.rb +4 -4
- data/lib/mneme/passive_recall.rb +2 -2
- data/lib/mneme/runner.rb +57 -75
- data/lib/mneme/search.rb +38 -38
- data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
- data/lib/mneme/tools/everything_ok.rb +1 -3
- data/lib/mneme/tools/save_snapshot.rb +12 -16
- data/lib/tools/bash.rb +4 -12
- data/lib/tools/edit.rb +4 -6
- data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
- data/lib/tools/read.rb +4 -4
- data/lib/tools/registry.rb +1 -1
- data/lib/tools/remember.rb +46 -55
- data/lib/tools/spawn_specialist.rb +12 -23
- data/lib/tools/spawn_subagent.rb +9 -19
- data/lib/tools/subagent_prompts.rb +0 -2
- data/lib/tools/think.rb +3 -10
- data/lib/tools/web_get.rb +23 -4
- data/lib/tools/write.rb +3 -3
- data/lib/tui/cable_client.rb +3 -3
- data/lib/tui/message_store.rb +37 -37
- data/lib/tui/screens/chat.rb +27 -15
- data/skills/activerecord/SKILL.md +1 -1
- data/skills/dragonruby/SKILL.md +1 -1
- data/skills/draper-decorators/SKILL.md +1 -1
- data/skills/gh-issue.md +1 -1
- data/skills/mcp-server/SKILL.md +1 -1
- data/skills/ratatui-ruby/SKILL.md +1 -1
- data/skills/rspec/SKILL.md +1 -1
- data/templates/config.toml +16 -5
- data/templates/soul.md +7 -19
- data/workflows/create_handoff.md +1 -1
- data/workflows/create_note.md +1 -1
- data/workflows/create_plan.md +1 -1
- data/workflows/implement_plan.md +1 -1
- data/workflows/iterate_plan.md +1 -1
- data/workflows/research_codebase.md +1 -1
- data/workflows/resume_handoff.md +1 -1
- data/workflows/review_pr.md +78 -16
- data/workflows/thoughts_init.md +1 -1
- data/workflows/validate_plan.md +1 -1
- metadata +10 -9
- data/app/jobs/count_event_tokens_job.rb +0 -39
- data/app/models/goal_pinned_event.rb +0 -11
- data/app/models/pinned_event.rb +0 -41
- data/lib/mneme/tools/attach_events_to_goals.rb +0 -107
|
@@ -3,32 +3,29 @@
|
|
|
3
3
|
require "open3"
|
|
4
4
|
|
|
5
5
|
module Tools
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# is tagged with the label from +[github] label+ in
|
|
9
|
-
#
|
|
6
|
+
# Opens a GitHub issue on Anima's repository via the +gh+ CLI,
|
|
7
|
+
# giving the agent a voice to report bugs, pain points, or ideas.
|
|
8
|
+
# Every issue is tagged with the label from +[github] label+ in
|
|
9
|
+
# +config.toml+ so maintainers can filter agent-originated issues.
|
|
10
10
|
#
|
|
11
11
|
# The repository is read from +[github] repo+ in +config.toml+; when
|
|
12
12
|
# unset, the tool falls back to parsing the +origin+ remote URL.
|
|
13
13
|
#
|
|
14
14
|
# @see https://github.com/hoblin/anima/issues/103
|
|
15
|
-
class
|
|
15
|
+
class OpenIssue < Base
|
|
16
16
|
# @return [String] tool identifier used in the Anthropic API schema
|
|
17
|
-
def self.tool_name = "
|
|
17
|
+
def self.tool_name = "open_issue"
|
|
18
18
|
|
|
19
|
-
# @return [String]
|
|
20
|
-
def self.description
|
|
21
|
-
"Don't have the right tool for this task? Request it! " \
|
|
22
|
-
"Creates a GitHub issue so the developer knows what you need."
|
|
23
|
-
end
|
|
19
|
+
# @return [String] description shown to the LLM
|
|
20
|
+
def self.description = "Something broken, missing, or could be better in Anima? Say it here."
|
|
24
21
|
|
|
25
22
|
# @return [Hash] JSON Schema for the tool's input parameters
|
|
26
23
|
def self.input_schema
|
|
27
24
|
{
|
|
28
25
|
type: "object",
|
|
29
26
|
properties: {
|
|
30
|
-
title: {type: "string"
|
|
31
|
-
description: {type: "string", description: "
|
|
27
|
+
title: {type: "string"},
|
|
28
|
+
description: {type: "string", description: "Use gh-issue skill for guidance."}
|
|
32
29
|
},
|
|
33
30
|
required: %w[title description]
|
|
34
31
|
}
|
data/lib/tools/read.rb
CHANGED
|
@@ -18,15 +18,15 @@ module Tools
|
|
|
18
18
|
class Read < Base
|
|
19
19
|
def self.tool_name = "read"
|
|
20
20
|
|
|
21
|
-
def self.description = "Read file
|
|
21
|
+
def self.description = "Read file. Relative paths resolve against working directory."
|
|
22
22
|
|
|
23
23
|
def self.input_schema
|
|
24
24
|
{
|
|
25
25
|
type: "object",
|
|
26
26
|
properties: {
|
|
27
|
-
path: {type: "string"
|
|
28
|
-
offset: {type: "integer", description: "1-indexed line number
|
|
29
|
-
limit: {type: "integer", description: "
|
|
27
|
+
path: {type: "string"},
|
|
28
|
+
offset: {type: "integer", description: "1-indexed line number (default: 1)."},
|
|
29
|
+
limit: {type: "integer", description: "Max lines to return."}
|
|
30
30
|
},
|
|
31
31
|
required: ["path"]
|
|
32
32
|
}
|
data/lib/tools/registry.rb
CHANGED
|
@@ -73,7 +73,7 @@ module Tools
|
|
|
73
73
|
s[:input_schema][:properties] ||= {}
|
|
74
74
|
s[:input_schema][:properties]["timeout"] = {
|
|
75
75
|
type: "integer",
|
|
76
|
-
description: "
|
|
76
|
+
description: "Seconds (default: #{default})."
|
|
77
77
|
}
|
|
78
78
|
s
|
|
79
79
|
end
|
data/lib/tools/remember.rb
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Tools
|
|
4
|
-
# Fractal-resolution zoom into
|
|
5
|
-
# on a target
|
|
4
|
+
# Fractal-resolution zoom into message history. Returns a window centered
|
|
5
|
+
# on a target message with full detail at the center and compressed context
|
|
6
6
|
# 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
|
-
# The agent discovers target
|
|
13
|
+
# The agent discovers target messages via FTS5 search results embedded in
|
|
14
14
|
# viewport recall snippets. This tool drills down into the full context.
|
|
15
15
|
#
|
|
16
16
|
# @example
|
|
17
|
-
# remember(
|
|
17
|
+
# remember(message_id: 42)
|
|
18
18
|
class Remember < Base
|
|
19
|
-
#
|
|
20
|
-
# ±10
|
|
19
|
+
# Messages around the target to include at full resolution.
|
|
20
|
+
# ±10 messages provides sharp foveal detail while keeping output readable.
|
|
21
21
|
CONTEXT_WINDOW = 20
|
|
22
22
|
|
|
23
23
|
ROLE_LABELS = {
|
|
@@ -28,24 +28,15 @@ module Tools
|
|
|
28
28
|
|
|
29
29
|
def self.tool_name = "remember"
|
|
30
30
|
|
|
31
|
-
def self.description
|
|
32
|
-
"Recall the full context around a past event. " \
|
|
33
|
-
"Returns a fractal-resolution window: high detail at the center " \
|
|
34
|
-
"(the target event and its neighbors), compressed context at the edges " \
|
|
35
|
-
"(snapshots before and after). Use this when search results surface " \
|
|
36
|
-
"a relevant event and you need the surrounding conversation."
|
|
37
|
-
end
|
|
31
|
+
def self.description = "Recall the full conversation around a past message."
|
|
38
32
|
|
|
39
33
|
def self.input_schema
|
|
40
34
|
{
|
|
41
35
|
type: "object",
|
|
42
36
|
properties: {
|
|
43
|
-
|
|
44
|
-
type: "integer",
|
|
45
|
-
description: "The event ID to zoom into (from search results or recall snippets)"
|
|
46
|
-
}
|
|
37
|
+
message_id: {type: "integer"}
|
|
47
38
|
},
|
|
48
|
-
required: ["
|
|
39
|
+
required: ["message_id"]
|
|
49
40
|
}
|
|
50
41
|
end
|
|
51
42
|
|
|
@@ -53,12 +44,12 @@ module Tools
|
|
|
53
44
|
@session = session
|
|
54
45
|
end
|
|
55
46
|
|
|
56
|
-
# @param input [Hash] with "
|
|
57
|
-
# @return [String] fractal-resolution window around the target
|
|
47
|
+
# @param input [Hash] with "message_id"
|
|
48
|
+
# @return [String] fractal-resolution window around the target message
|
|
58
49
|
def execute(input)
|
|
59
|
-
|
|
60
|
-
target =
|
|
61
|
-
return {error: "
|
|
50
|
+
message_id = input["message_id"].to_i
|
|
51
|
+
target = Message.find_by(id: message_id)
|
|
52
|
+
return {error: "Message #{message_id} not found"} unless target
|
|
62
53
|
|
|
63
54
|
build_fractal_window(target)
|
|
64
55
|
end
|
|
@@ -67,17 +58,17 @@ module Tools
|
|
|
67
58
|
|
|
68
59
|
# Assembles the three-zone fractal window.
|
|
69
60
|
#
|
|
70
|
-
# @param target [
|
|
61
|
+
# @param target [Message] the center message
|
|
71
62
|
# @return [String] formatted fractal window
|
|
72
63
|
def build_fractal_window(target)
|
|
73
64
|
target_session = target.session
|
|
74
|
-
|
|
75
|
-
first_center_id =
|
|
76
|
-
last_center_id =
|
|
65
|
+
center_messages = fetch_center_messages(target, target_session)
|
|
66
|
+
first_center_id = center_messages.first&.id
|
|
67
|
+
last_center_id = center_messages.last&.id
|
|
77
68
|
|
|
78
69
|
sections = build_sections(
|
|
79
70
|
target_session: target_session,
|
|
80
|
-
|
|
71
|
+
center_messages: center_messages,
|
|
81
72
|
target_id: target.id,
|
|
82
73
|
first_center_id: first_center_id,
|
|
83
74
|
last_center_id: last_center_id
|
|
@@ -86,18 +77,18 @@ module Tools
|
|
|
86
77
|
end
|
|
87
78
|
|
|
88
79
|
# Builds ordered sections: header, before snapshots, center, after snapshots.
|
|
89
|
-
def build_sections(target_session:,
|
|
80
|
+
def build_sections(target_session:, center_messages:, target_id:, first_center_id:, last_center_id:)
|
|
90
81
|
sections = [session_header(target_session)]
|
|
91
82
|
|
|
92
83
|
append_snapshot_sections(sections, target_session.snapshots
|
|
93
|
-
.where("
|
|
84
|
+
.where("to_message_id < ?", first_center_id)
|
|
94
85
|
.chronological.last(3), label: "PREVIOUS CONTEXT")
|
|
95
86
|
|
|
96
|
-
sections << "── FULL CONTEXT (
|
|
97
|
-
|
|
87
|
+
sections << "── FULL CONTEXT (messages #{first_center_id}..#{last_center_id}) ──"
|
|
88
|
+
center_messages.each { |msg| sections << render_center_message(msg, target_id) }
|
|
98
89
|
|
|
99
90
|
append_snapshot_sections(sections, target_session.snapshots
|
|
100
|
-
.where("
|
|
91
|
+
.where("from_message_id > ?", last_center_id)
|
|
101
92
|
.chronological.first(3), label: "FOLLOWING CONTEXT")
|
|
102
93
|
|
|
103
94
|
sections
|
|
@@ -116,12 +107,12 @@ module Tools
|
|
|
116
107
|
snapshots.each { |snapshot| sections << format_snapshot(snapshot) }
|
|
117
108
|
end
|
|
118
109
|
|
|
119
|
-
# Fetches conversation
|
|
110
|
+
# Fetches conversation messages around the target within a fixed window.
|
|
120
111
|
#
|
|
121
|
-
# @return [Array<
|
|
122
|
-
def
|
|
112
|
+
# @return [Array<Message>] chronologically ordered
|
|
113
|
+
def fetch_center_messages(target, target_session)
|
|
123
114
|
half = CONTEXT_WINDOW / 2
|
|
124
|
-
scope = target_session.
|
|
115
|
+
scope = target_session.messages.context_messages.deliverable
|
|
125
116
|
target_id = target.id
|
|
126
117
|
|
|
127
118
|
before = scope.where("id <= ?", target_id).reorder(id: :desc).limit(half + 1).to_a.reverse
|
|
@@ -130,37 +121,37 @@ module Tools
|
|
|
130
121
|
before + after
|
|
131
122
|
end
|
|
132
123
|
|
|
133
|
-
# Renders a center
|
|
134
|
-
# Conversation
|
|
124
|
+
# Renders a center message at full resolution.
|
|
125
|
+
# Conversation messages show full content. Tool calls show name + input.
|
|
135
126
|
# Tool responses compressed to status indicator.
|
|
136
127
|
#
|
|
137
|
-
# @param
|
|
138
|
-
# @param target_id [Integer] the
|
|
128
|
+
# @param message [Message]
|
|
129
|
+
# @param target_id [Integer] the message being zoomed into (marked with arrow)
|
|
139
130
|
# @return [String]
|
|
140
|
-
def
|
|
141
|
-
marker = (
|
|
142
|
-
prefix = "#{marker}
|
|
131
|
+
def render_center_message(message, target_id)
|
|
132
|
+
marker = (message.id == target_id) ? "→" : " "
|
|
133
|
+
prefix = "#{marker} message #{message.id}"
|
|
143
134
|
|
|
144
|
-
"#{prefix} #{
|
|
135
|
+
"#{prefix} #{format_message_content(message)}"
|
|
145
136
|
end
|
|
146
137
|
|
|
147
|
-
# Formats
|
|
148
|
-
def
|
|
149
|
-
data =
|
|
138
|
+
# Formats message content based on type.
|
|
139
|
+
def format_message_content(message)
|
|
140
|
+
data = message.payload
|
|
150
141
|
content = data["content"]
|
|
151
142
|
|
|
152
|
-
if ROLE_LABELS.key?(
|
|
153
|
-
"#{ROLE_LABELS[
|
|
154
|
-
elsif
|
|
143
|
+
if ROLE_LABELS.key?(message.message_type)
|
|
144
|
+
"#{ROLE_LABELS[message.message_type]}: #{content}"
|
|
145
|
+
elsif message.message_type == "tool_call"
|
|
155
146
|
format_tool_call(data)
|
|
156
|
-
elsif
|
|
147
|
+
elsif message.message_type == "tool_response"
|
|
157
148
|
status = content.to_s.start_with?("Error") ? "error" : "ok"
|
|
158
149
|
"ToolResult: [#{status}] #{data["tool_use_id"]}"
|
|
159
150
|
end
|
|
160
151
|
end
|
|
161
152
|
|
|
162
153
|
def format_tool_call(data)
|
|
163
|
-
if data["tool_name"] ==
|
|
154
|
+
if data["tool_name"] == Message::THINK_TOOL
|
|
164
155
|
"Think: #{data.dig("tool_input", "thoughts")}"
|
|
165
156
|
else
|
|
166
157
|
"Tool: #{data["tool_name"]}(#{data["tool_input"].to_json.truncate(200)})"
|
|
@@ -173,7 +164,7 @@ module Tools
|
|
|
173
164
|
# @return [String]
|
|
174
165
|
def format_snapshot(snapshot)
|
|
175
166
|
level = (snapshot.level == 2) ? "L2" : "L1"
|
|
176
|
-
"[#{level} snapshot,
|
|
167
|
+
"[#{level} snapshot, messages #{snapshot.from_message_id}..#{snapshot.to_message_id}]\n#{snapshot.text}"
|
|
177
168
|
end
|
|
178
169
|
end
|
|
179
170
|
end
|
|
@@ -21,9 +21,8 @@ module Tools
|
|
|
21
21
|
|
|
22
22
|
# Builds description dynamically to include available specialists.
|
|
23
23
|
def self.description
|
|
24
|
-
base = "Spawn a
|
|
25
|
-
"
|
|
26
|
-
"Its text messages are forwarded to you automatically. " \
|
|
24
|
+
base = "Spawn a specialist to work on a task. " \
|
|
25
|
+
"Its messages are forwarded to you. " \
|
|
27
26
|
"Address it via @name to send follow-up instructions."
|
|
28
27
|
|
|
29
28
|
registry = Agents::Registry.instance
|
|
@@ -39,16 +38,9 @@ module Tools
|
|
|
39
38
|
type: "object",
|
|
40
39
|
properties: {
|
|
41
40
|
name: name_property,
|
|
42
|
-
task: {
|
|
43
|
-
type: "string",
|
|
44
|
-
description: "What the specialist should do (persisted as its first user message)"
|
|
45
|
-
},
|
|
46
|
-
expected_output: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "Description of the expected deliverable"
|
|
49
|
-
}
|
|
41
|
+
task: {type: "string", description: "State the goal — the specialist knows its method."}
|
|
50
42
|
},
|
|
51
|
-
required: %w[name task
|
|
43
|
+
required: %w[name task]
|
|
52
44
|
}
|
|
53
45
|
end
|
|
54
46
|
|
|
@@ -57,7 +49,7 @@ module Tools
|
|
|
57
49
|
registry = Agents::Registry.instance
|
|
58
50
|
prop = {
|
|
59
51
|
type: "string",
|
|
60
|
-
description: "
|
|
52
|
+
description: "Specialist to spawn."
|
|
61
53
|
}
|
|
62
54
|
prop[:enum] = registry.names if registry.any?
|
|
63
55
|
prop
|
|
@@ -76,22 +68,20 @@ module Tools
|
|
|
76
68
|
# persists the task as a user message, and queues background processing.
|
|
77
69
|
# Returns immediately (non-blocking).
|
|
78
70
|
#
|
|
79
|
-
# @param input [Hash<String, Object>] with "name"
|
|
71
|
+
# @param input [Hash<String, Object>] with "name" and "task"
|
|
80
72
|
# @return [String] confirmation with child session ID
|
|
81
73
|
# @return [Hash{Symbol => String}] with :error key on validation failure
|
|
82
74
|
def execute(input)
|
|
83
75
|
task = input["task"].to_s.strip
|
|
84
|
-
expected_output = input["expected_output"].to_s.strip
|
|
85
76
|
name = input["name"].to_s.strip
|
|
86
77
|
|
|
87
78
|
return {error: "Name cannot be blank"} if name.empty?
|
|
88
79
|
return {error: "Task cannot be blank"} if task.empty?
|
|
89
|
-
return {error: "Expected output cannot be blank"} if expected_output.empty?
|
|
90
80
|
|
|
91
81
|
definition = @agent_registry.get(name)
|
|
92
82
|
return {error: "Unknown agent: #{name}"} unless definition
|
|
93
83
|
|
|
94
|
-
child = spawn_child(definition, task
|
|
84
|
+
child = spawn_child(definition, task)
|
|
95
85
|
nickname = child.name
|
|
96
86
|
"Specialist @#{nickname} spawned (session #{child.id}). " \
|
|
97
87
|
"Its messages will appear in your conversation. " \
|
|
@@ -100,22 +90,21 @@ module Tools
|
|
|
100
90
|
|
|
101
91
|
private
|
|
102
92
|
|
|
103
|
-
def spawn_child(definition, task
|
|
104
|
-
prompt = build_prompt(definition, expected_output)
|
|
93
|
+
def spawn_child(definition, task)
|
|
105
94
|
child = Session.create!(
|
|
106
95
|
parent_session_id: @session.id,
|
|
107
|
-
prompt:
|
|
96
|
+
prompt: build_prompt(definition),
|
|
108
97
|
granted_tools: definition.tools
|
|
109
98
|
)
|
|
110
|
-
child.
|
|
99
|
+
child.create_user_message(task)
|
|
111
100
|
assign_nickname_via_brain(child)
|
|
112
101
|
child.broadcast_children_update_to_parent
|
|
113
102
|
AgentRequestJob.perform_later(child.id)
|
|
114
103
|
child
|
|
115
104
|
end
|
|
116
105
|
|
|
117
|
-
def build_prompt(definition
|
|
118
|
-
"#{definition.prompt}\n\n#{COMMUNICATION_INSTRUCTION}
|
|
106
|
+
def build_prompt(definition)
|
|
107
|
+
"#{definition.prompt}\n\n#{COMMUNICATION_INSTRUCTION}"
|
|
119
108
|
end
|
|
120
109
|
end
|
|
121
110
|
end
|
data/lib/tools/spawn_subagent.rb
CHANGED
|
@@ -19,9 +19,8 @@ module Tools
|
|
|
19
19
|
def self.tool_name = "spawn_subagent"
|
|
20
20
|
|
|
21
21
|
def self.description
|
|
22
|
-
"Spawn a
|
|
23
|
-
"
|
|
24
|
-
"and its text messages are forwarded to you automatically. " \
|
|
22
|
+
"Spawn a sub-agent to work on a task. " \
|
|
23
|
+
"It inherits your conversation context and its messages are forwarded to you. " \
|
|
25
24
|
"Address it via @nickname to send follow-up instructions."
|
|
26
25
|
end
|
|
27
26
|
|
|
@@ -29,14 +28,7 @@ module Tools
|
|
|
29
28
|
{
|
|
30
29
|
type: "object",
|
|
31
30
|
properties: {
|
|
32
|
-
task: {
|
|
33
|
-
type: "string",
|
|
34
|
-
description: "What the sub-agent should do (persisted as its first user message)"
|
|
35
|
-
},
|
|
36
|
-
expected_output: {
|
|
37
|
-
type: "string",
|
|
38
|
-
description: "Description of the expected deliverable"
|
|
39
|
-
},
|
|
31
|
+
task: {type: "string"},
|
|
40
32
|
tools: {
|
|
41
33
|
type: "array",
|
|
42
34
|
items: {type: "string"},
|
|
@@ -45,7 +37,7 @@ module Tools
|
|
|
45
37
|
"Valid tools: #{AgentLoop::STANDARD_TOOLS_BY_NAME.keys.join(", ")}"
|
|
46
38
|
}
|
|
47
39
|
},
|
|
48
|
-
required: %w[task
|
|
40
|
+
required: %w[task]
|
|
49
41
|
}
|
|
50
42
|
end
|
|
51
43
|
|
|
@@ -58,22 +50,20 @@ module Tools
|
|
|
58
50
|
# persists the task as a user message, and queues background processing.
|
|
59
51
|
# Returns immediately after brain completes (blocking for ~200ms).
|
|
60
52
|
#
|
|
61
|
-
# @param input [Hash<String, Object>] with "task"
|
|
53
|
+
# @param input [Hash<String, Object>] with "task" and optional "tools"
|
|
62
54
|
# @return [String] confirmation with child session ID and @nickname
|
|
63
55
|
# @return [Hash{Symbol => String}] with :error key on validation failure
|
|
64
56
|
def execute(input)
|
|
65
57
|
task = input["task"].to_s.strip
|
|
66
|
-
expected_output = input["expected_output"].to_s.strip
|
|
67
58
|
|
|
68
59
|
return {error: "Task cannot be blank"} if task.empty?
|
|
69
|
-
return {error: "Expected output cannot be blank"} if expected_output.empty?
|
|
70
60
|
|
|
71
61
|
tools = normalize_tools(input["tools"])
|
|
72
62
|
|
|
73
63
|
error = validate_tools(tools)
|
|
74
64
|
return error if error
|
|
75
65
|
|
|
76
|
-
child = spawn_child(task,
|
|
66
|
+
child = spawn_child(task, tools)
|
|
77
67
|
nickname = child.name
|
|
78
68
|
"Sub-agent @#{nickname} spawned (session #{child.id}). " \
|
|
79
69
|
"Its messages will appear in your conversation. " \
|
|
@@ -82,13 +72,13 @@ module Tools
|
|
|
82
72
|
|
|
83
73
|
private
|
|
84
74
|
|
|
85
|
-
def spawn_child(task,
|
|
75
|
+
def spawn_child(task, granted_tools)
|
|
86
76
|
child = Session.create!(
|
|
87
77
|
parent_session_id: @session.id,
|
|
88
|
-
prompt:
|
|
78
|
+
prompt: GENERIC_PROMPT,
|
|
89
79
|
granted_tools: granted_tools
|
|
90
80
|
)
|
|
91
|
-
child.
|
|
81
|
+
child.create_user_message(task)
|
|
92
82
|
assign_nickname_via_brain(child)
|
|
93
83
|
child.broadcast_children_update_to_parent
|
|
94
84
|
AgentRequestJob.perform_later(child.id)
|
|
@@ -8,8 +8,6 @@ module Tools
|
|
|
8
8
|
"When you finish, write your final summary and stop — no special tool needed. " \
|
|
9
9
|
"If you need clarification, just ask — the parent can reply."
|
|
10
10
|
|
|
11
|
-
EXPECTED_DELIVERABLE_PREFIX = "Expected deliverable: "
|
|
12
|
-
|
|
13
11
|
private
|
|
14
12
|
|
|
15
13
|
# Runs the analytical brain synchronously to assign a nickname.
|
data/lib/tools/think.rb
CHANGED
|
@@ -21,24 +21,17 @@ module Tools
|
|
|
21
21
|
class Think < Base
|
|
22
22
|
def self.tool_name = "think"
|
|
23
23
|
|
|
24
|
-
def self.description
|
|
25
|
-
"Express your internal reasoning between tool calls. " \
|
|
26
|
-
"Use this to analyze intermediate results, plan next steps, or make decisions before continuing. " \
|
|
27
|
-
"Set visibility to \"aloud\" when you want the user to see your thought process."
|
|
28
|
-
end
|
|
24
|
+
def self.description = "Think out loud or silently."
|
|
29
25
|
|
|
30
26
|
def self.input_schema
|
|
31
27
|
{
|
|
32
28
|
type: "object",
|
|
33
29
|
properties: {
|
|
34
|
-
thoughts: {
|
|
35
|
-
type: "string",
|
|
36
|
-
description: "Your reasoning, analysis, or internal monologue"
|
|
37
|
-
},
|
|
30
|
+
thoughts: {type: "string"},
|
|
38
31
|
visibility: {
|
|
39
32
|
type: "string",
|
|
40
33
|
enum: ["inner", "aloud"],
|
|
41
|
-
description: "
|
|
34
|
+
description: "inner (default) is silent. aloud is shown to the user."
|
|
42
35
|
}
|
|
43
36
|
},
|
|
44
37
|
required: ["thoughts"]
|
data/lib/tools/web_get.rb
CHANGED
|
@@ -15,13 +15,13 @@ module Tools
|
|
|
15
15
|
class WebGet < Base
|
|
16
16
|
def self.tool_name = "web_get"
|
|
17
17
|
|
|
18
|
-
def self.description = "Fetch
|
|
18
|
+
def self.description = "Fetch a URL."
|
|
19
19
|
|
|
20
20
|
def self.input_schema
|
|
21
21
|
{
|
|
22
22
|
type: "object",
|
|
23
23
|
properties: {
|
|
24
|
-
url: {type: "string"
|
|
24
|
+
url: {type: "string"}
|
|
25
25
|
},
|
|
26
26
|
required: ["url"]
|
|
27
27
|
}
|
|
@@ -47,10 +47,11 @@ module Tools
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
response = HTTParty.get(url, timeout: timeout, follow_redirects: false, ssl_ca_file: Certifi.where)
|
|
50
|
-
body = truncate_body(response.body.to_s)
|
|
51
50
|
content_type = response.content_type || "text/plain"
|
|
51
|
+
body = response.body.to_s
|
|
52
|
+
body = strip_html_noise(body) if content_type.include?("text/html")
|
|
52
53
|
|
|
53
|
-
{body: body, content_type: content_type}
|
|
54
|
+
{body: truncate_body(body), content_type: content_type}
|
|
54
55
|
rescue URI::InvalidURIError => error
|
|
55
56
|
{error: "Invalid URL: #{error.message}"}
|
|
56
57
|
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
@@ -68,5 +69,23 @@ module Tools
|
|
|
68
69
|
body.byteslice(0, max_bytes).scrub +
|
|
69
70
|
"\n\n[Truncated: response exceeded #{max_bytes} bytes]"
|
|
70
71
|
end
|
|
72
|
+
|
|
73
|
+
# First-stage noise stripping — runs before truncation so that the
|
|
74
|
+
# byte budget is spent on content, not on scripts/SVGs/metadata.
|
|
75
|
+
# Each pattern targets one tag type for easy maintenance.
|
|
76
|
+
# The decorator applies a second, structure-aware pass via Nokogiri.
|
|
77
|
+
HTML_NOISE_PATTERNS = [
|
|
78
|
+
%r{<head\b[^>]*>.*?</head>}mi, # metadata, link/meta tags
|
|
79
|
+
%r{<script\b[^>]*>.*?</script>}mi, # JavaScript
|
|
80
|
+
%r{<style\b[^>]*>.*?</style>}mi, # CSS
|
|
81
|
+
%r{<svg\b[^>]*>.*?</svg>}mi, # inline graphics
|
|
82
|
+
%r{<template\b[^>]*>.*?</template>}mi, # deferred markup
|
|
83
|
+
%r{<noscript\b[^>]*>.*?</noscript>}mi # JS-disabled fallbacks
|
|
84
|
+
].freeze
|
|
85
|
+
private_constant :HTML_NOISE_PATTERNS
|
|
86
|
+
|
|
87
|
+
def strip_html_noise(html)
|
|
88
|
+
HTML_NOISE_PATTERNS.reduce(html) { |text, pattern| text.gsub(pattern, "") }
|
|
89
|
+
end
|
|
71
90
|
end
|
|
72
91
|
end
|
data/lib/tools/write.rb
CHANGED
|
@@ -17,14 +17,14 @@ module Tools
|
|
|
17
17
|
class Write < Base
|
|
18
18
|
def self.tool_name = "write"
|
|
19
19
|
|
|
20
|
-
def self.description = "
|
|
20
|
+
def self.description = "Write file."
|
|
21
21
|
|
|
22
22
|
def self.input_schema
|
|
23
23
|
{
|
|
24
24
|
type: "object",
|
|
25
25
|
properties: {
|
|
26
|
-
path: {type: "string", description: "
|
|
27
|
-
content: {type: "string"
|
|
26
|
+
path: {type: "string", description: "Relative paths resolve against working directory. Creates intermediate directories."},
|
|
27
|
+
content: {type: "string"}
|
|
28
28
|
},
|
|
29
29
|
required: %w[path content]
|
|
30
30
|
}
|
data/lib/tui/cable_client.rb
CHANGED
|
@@ -129,9 +129,9 @@ module TUI
|
|
|
129
129
|
# Requests the brain to recall (delete) a pending message so the user
|
|
130
130
|
# can edit it before the LLM sees it.
|
|
131
131
|
#
|
|
132
|
-
# @param
|
|
133
|
-
def recall_pending(
|
|
134
|
-
send_action("recall_pending", {"
|
|
132
|
+
# @param message_id [Integer] database ID of the pending user_message
|
|
133
|
+
def recall_pending(message_id)
|
|
134
|
+
send_action("recall_pending", {"message_id" => message_id})
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# Requests interruption of the current tool execution. The server sets
|