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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +2 -0
  3. data/agents/codebase-analyzer.md +1 -1
  4. data/agents/codebase-pattern-finder.md +1 -1
  5. data/agents/documentation-researcher.md +1 -1
  6. data/agents/thoughts-analyzer.md +1 -1
  7. data/agents/web-search-researcher.md +1 -1
  8. data/app/channels/session_channel.rb +44 -43
  9. data/app/decorators/agent_message_decorator.rb +2 -2
  10. data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
  11. data/app/decorators/system_message_decorator.rb +2 -2
  12. data/app/decorators/tool_call_decorator.rb +2 -2
  13. data/app/decorators/tool_decorator.rb +4 -4
  14. data/app/decorators/tool_response_decorator.rb +2 -2
  15. data/app/decorators/user_message_decorator.rb +3 -3
  16. data/app/decorators/web_get_tool_decorator.rb +41 -9
  17. data/app/jobs/agent_request_job.rb +20 -20
  18. data/app/jobs/count_message_tokens_job.rb +39 -0
  19. data/app/jobs/passive_recall_job.rb +4 -4
  20. data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
  21. data/app/models/goal.rb +4 -4
  22. data/app/models/goal_pinned_message.rb +11 -0
  23. data/app/models/{event.rb → message.rb} +42 -39
  24. data/app/models/pinned_message.rb +41 -0
  25. data/app/models/session.rb +206 -198
  26. data/app/models/snapshot.rb +25 -25
  27. data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
  28. data/lib/agent_loop.rb +6 -6
  29. data/lib/analytical_brain/runner.rb +35 -35
  30. data/lib/analytical_brain/tools/activate_skill.rb +5 -9
  31. data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
  32. data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
  33. data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
  34. data/lib/analytical_brain/tools/finish_goal.rb +5 -8
  35. data/lib/analytical_brain/tools/read_workflow.rb +5 -9
  36. data/lib/analytical_brain/tools/rename_session.rb +3 -10
  37. data/lib/analytical_brain/tools/set_goal.rb +3 -7
  38. data/lib/analytical_brain/tools/update_goal.rb +3 -7
  39. data/lib/anima/settings.rb +15 -4
  40. data/lib/anima/version.rb +1 -1
  41. data/lib/events/bounce_back.rb +7 -7
  42. data/lib/events/subscribers/persister.rb +7 -7
  43. data/lib/events/subscribers/transient_broadcaster.rb +2 -2
  44. data/lib/mneme/compressed_viewport.rb +57 -57
  45. data/lib/mneme/l2_runner.rb +4 -4
  46. data/lib/mneme/passive_recall.rb +2 -2
  47. data/lib/mneme/runner.rb +57 -75
  48. data/lib/mneme/search.rb +38 -38
  49. data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
  50. data/lib/mneme/tools/everything_ok.rb +1 -3
  51. data/lib/mneme/tools/save_snapshot.rb +12 -16
  52. data/lib/tools/bash.rb +4 -12
  53. data/lib/tools/edit.rb +4 -6
  54. data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
  55. data/lib/tools/read.rb +4 -4
  56. data/lib/tools/registry.rb +1 -1
  57. data/lib/tools/remember.rb +46 -55
  58. data/lib/tools/spawn_specialist.rb +12 -23
  59. data/lib/tools/spawn_subagent.rb +9 -19
  60. data/lib/tools/subagent_prompts.rb +0 -2
  61. data/lib/tools/think.rb +3 -10
  62. data/lib/tools/web_get.rb +23 -4
  63. data/lib/tools/write.rb +3 -3
  64. data/lib/tui/cable_client.rb +3 -3
  65. data/lib/tui/message_store.rb +37 -37
  66. data/lib/tui/screens/chat.rb +27 -15
  67. data/skills/activerecord/SKILL.md +1 -1
  68. data/skills/dragonruby/SKILL.md +1 -1
  69. data/skills/draper-decorators/SKILL.md +1 -1
  70. data/skills/gh-issue.md +1 -1
  71. data/skills/mcp-server/SKILL.md +1 -1
  72. data/skills/ratatui-ruby/SKILL.md +1 -1
  73. data/skills/rspec/SKILL.md +1 -1
  74. data/templates/config.toml +16 -5
  75. data/templates/soul.md +7 -19
  76. data/workflows/create_handoff.md +1 -1
  77. data/workflows/create_note.md +1 -1
  78. data/workflows/create_plan.md +1 -1
  79. data/workflows/implement_plan.md +1 -1
  80. data/workflows/iterate_plan.md +1 -1
  81. data/workflows/research_codebase.md +1 -1
  82. data/workflows/resume_handoff.md +1 -1
  83. data/workflows/review_pr.md +78 -16
  84. data/workflows/thoughts_init.md +1 -1
  85. data/workflows/validate_plan.md +1 -1
  86. metadata +10 -9
  87. data/app/jobs/count_event_tokens_job.rb +0 -39
  88. data/app/models/goal_pinned_event.rb +0 -11
  89. data/app/models/pinned_event.rb +0 -41
  90. 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
- # Creates a GitHub issue via the +gh+ CLI, letting the agent request
7
- # capabilities it discovers are missing during real work. Every issue
8
- # is tagged with the label from +[github] label+ in +config.toml+ so
9
- # the developer can filter agent-originated requests from human ones.
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 RequestFeature < Base
15
+ class OpenIssue < Base
16
16
  # @return [String] tool identifier used in the Anthropic API schema
17
- def self.tool_name = "request_feature"
17
+ def self.tool_name = "open_issue"
18
18
 
19
- # @return [String] motivational description shown to the LLM
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", description: "Short, descriptive title for the feature request"},
31
- description: {type: "string", description: "What you need and why — what were you trying to do, and what's missing?"}
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 contents. Returns plain text with smart truncation. Use offset/limit to page through large files."
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", description: "Absolute or relative file path (relative resolved against working directory)"},
28
- offset: {type: "integer", description: "1-indexed line number to start from (default: 1)"},
29
- limit: {type: "integer", description: "Maximum lines to read (subject to line and byte caps from config)"}
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
  }
@@ -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: "Max execution seconds (default: #{default}). Increase for long-running operations."
76
+ description: "Seconds (default: #{default})."
77
77
  }
78
78
  s
79
79
  end
@@ -1,23 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tools
4
- # Fractal-resolution zoom into event history. Returns a window centered
5
- # on a target event with full detail at the center and compressed context
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
- # [Events N-M — full detail, tool_responses compressed to checkmarks]
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 events via FTS5 search results embedded in
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(event_id: 42)
17
+ # remember(message_id: 42)
18
18
  class Remember < Base
19
- # Events around the target to include at full resolution.
20
- # ±10 events provides sharp foveal detail while keeping output readable.
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
- event_id: {
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: ["event_id"]
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 "event_id"
57
- # @return [String] fractal-resolution window around the target event
47
+ # @param input [Hash] with "message_id"
48
+ # @return [String] fractal-resolution window around the target message
58
49
  def execute(input)
59
- event_id = input["event_id"].to_i
60
- target = Event.find_by(id: event_id)
61
- return {error: "Event #{event_id} not found"} unless target
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 [Event] the center event
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
- center_events = fetch_center_events(target, target_session)
75
- first_center_id = center_events.first&.id
76
- last_center_id = center_events.last&.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
- center_events: center_events,
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:, center_events:, target_id:, first_center_id:, last_center_id:)
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("to_event_id < ?", first_center_id)
84
+ .where("to_message_id < ?", first_center_id)
94
85
  .chronological.last(3), label: "PREVIOUS CONTEXT")
95
86
 
96
- sections << "── FULL CONTEXT (events #{first_center_id}..#{last_center_id}) ──"
97
- center_events.each { |event| sections << render_center_event(event, target_id) }
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("from_event_id > ?", last_center_id)
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 events around the target within a fixed window.
110
+ # Fetches conversation messages around the target within a fixed window.
120
111
  #
121
- # @return [Array<Event>] chronologically ordered
122
- def fetch_center_events(target, target_session)
112
+ # @return [Array<Message>] chronologically ordered
113
+ def fetch_center_messages(target, target_session)
123
114
  half = CONTEXT_WINDOW / 2
124
- scope = target_session.events.context_events.deliverable
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 event at full resolution.
134
- # Conversation events show full content. Tool calls show name + input.
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 event [Event]
138
- # @param target_id [Integer] the event being zoomed into (marked with arrow)
128
+ # @param message [Message]
129
+ # @param target_id [Integer] the message being zoomed into (marked with arrow)
139
130
  # @return [String]
140
- def render_center_event(event, target_id)
141
- marker = (event.id == target_id) ? "→" : " "
142
- prefix = "#{marker} event #{event.id}"
131
+ def render_center_message(message, target_id)
132
+ marker = (message.id == target_id) ? "→" : " "
133
+ prefix = "#{marker} message #{message.id}"
143
134
 
144
- "#{prefix} #{format_event_content(event)}"
135
+ "#{prefix} #{format_message_content(message)}"
145
136
  end
146
137
 
147
- # Formats event content based on type.
148
- def format_event_content(event)
149
- data = event.payload
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?(event.event_type)
153
- "#{ROLE_LABELS[event.event_type]}: #{content}"
154
- elsif event.event_type == "tool_call"
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 event.event_type == "tool_response"
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"] == Event::THINK_TOOL
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, events #{snapshot.from_event_id}..#{snapshot.to_event_id}]\n#{snapshot.text}"
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 named specialist sub-agent to work on a task autonomously. " \
25
- "The specialist has a predefined role, system prompt, and tool set. " \
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 expected_output]
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: "Named specialist agent to spawn from the registry."
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", "task", and "expected_output"
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, expected_output)
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, expected_output)
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: prompt,
96
+ prompt: build_prompt(definition),
108
97
  granted_tools: definition.tools
109
98
  )
110
- child.create_user_event(task)
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, expected_output)
118
- "#{definition.prompt}\n\n#{COMMUNICATION_INSTRUCTION}\n\n#{EXPECTED_DELIVERABLE_PREFIX}#{expected_output}"
106
+ def build_prompt(definition)
107
+ "#{definition.prompt}\n\n#{COMMUNICATION_INSTRUCTION}"
119
108
  end
120
109
  end
121
110
  end
@@ -19,9 +19,8 @@ module Tools
19
19
  def self.tool_name = "spawn_subagent"
20
20
 
21
21
  def self.description
22
- "Spawn a generic sub-agent to work on a task autonomously. " \
23
- "The sub-agent inherits your conversation context, works independently, " \
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 expected_output]
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", "expected_output", and optional "tools"
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, expected_output, tools)
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, expected_output, granted_tools)
75
+ def spawn_child(task, granted_tools)
86
76
  child = Session.create!(
87
77
  parent_session_id: @session.id,
88
- prompt: "#{GENERIC_PROMPT}\n#{EXPECTED_DELIVERABLE_PREFIX}#{expected_output}",
78
+ prompt: GENERIC_PROMPT,
89
79
  granted_tools: granted_tools
90
80
  )
91
- child.create_user_event(task)
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: "\"inner\" (default) for silent reasoning; \"aloud\" to narrate for the user"
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 content from a URL via HTTP GET and return the response body"
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", description: "The URL to fetch (http or https)"}
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 = "Create or overwrite a file. Creates intermediate directories automatically. Use for new files or full replacement."
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: "Absolute or relative file path (relative resolved against working directory)"},
27
- content: {type: "string", description: "Full file content to write"}
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
  }
@@ -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 event_id [Integer] database ID of the pending user_message event
133
- def recall_pending(event_id)
134
- send_action("recall_pending", {"event_id" => event_id})
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