anima-core 1.0.2 → 1.1.1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +1 -0
  3. data/.reek.yml +51 -0
  4. data/README.md +63 -29
  5. data/anima-core.gemspec +4 -1
  6. data/app/channels/session_channel.rb +30 -11
  7. data/app/decorators/tool_call_decorator.rb +32 -3
  8. data/app/decorators/tool_decorator.rb +57 -0
  9. data/app/decorators/tool_response_decorator.rb +12 -4
  10. data/app/decorators/web_get_tool_decorator.rb +102 -0
  11. data/app/jobs/agent_request_job.rb +93 -23
  12. data/app/jobs/mneme_job.rb +51 -0
  13. data/app/jobs/passive_recall_job.rb +29 -0
  14. data/app/models/concerns/event/broadcasting.rb +4 -0
  15. data/app/models/event.rb +10 -0
  16. data/app/models/goal.rb +27 -0
  17. data/app/models/goal_pinned_event.rb +11 -0
  18. data/app/models/pinned_event.rb +41 -0
  19. data/app/models/session.rb +402 -6
  20. data/app/models/snapshot.rb +76 -0
  21. data/bin/jobs +5 -0
  22. data/config/initializers/event_subscribers.rb +12 -3
  23. data/config/initializers/fts5_schema_dump.rb +21 -0
  24. data/config/queue.yml +0 -1
  25. data/db/migrate/20260321080000_create_mneme_schema.rb +32 -0
  26. data/db/migrate/20260321120000_create_pinned_events.rb +27 -0
  27. data/db/migrate/20260321140000_create_events_fts_index.rb +77 -0
  28. data/db/migrate/20260321140100_add_recalled_event_ids_to_sessions.rb +10 -0
  29. data/lib/agent_loop.rb +63 -20
  30. data/lib/analytical_brain/runner.rb +158 -65
  31. data/lib/analytical_brain/tools/assign_nickname.rb +76 -0
  32. data/lib/analytical_brain/tools/finish_goal.rb +6 -1
  33. data/lib/anima/cli.rb +32 -9
  34. data/lib/anima/installer.rb +11 -24
  35. data/lib/anima/settings.rb +59 -0
  36. data/lib/anima/spinner.rb +75 -0
  37. data/lib/anima/version.rb +1 -1
  38. data/lib/environment_probe.rb +4 -4
  39. data/lib/events/bounce_back.rb +37 -0
  40. data/lib/events/subscribers/persister.rb +19 -0
  41. data/lib/events/subscribers/subagent_message_router.rb +102 -0
  42. data/lib/events/subscribers/transient_broadcaster.rb +36 -0
  43. data/lib/events/tool_call.rb +5 -3
  44. data/lib/llm/client.rb +19 -9
  45. data/lib/mneme/compressed_viewport.rb +200 -0
  46. data/lib/mneme/l2_runner.rb +138 -0
  47. data/lib/mneme/passive_recall.rb +69 -0
  48. data/lib/mneme/runner.rb +254 -0
  49. data/lib/mneme/search.rb +150 -0
  50. data/lib/mneme/tools/attach_events_to_goals.rb +107 -0
  51. data/lib/mneme/tools/everything_ok.rb +24 -0
  52. data/lib/mneme/tools/save_snapshot.rb +68 -0
  53. data/lib/mneme.rb +29 -0
  54. data/lib/providers/anthropic.rb +57 -13
  55. data/lib/shell_session.rb +194 -63
  56. data/lib/tasks/fts5.rake +6 -0
  57. data/lib/tools/base.rb +2 -1
  58. data/lib/tools/bash.rb +4 -2
  59. data/lib/tools/registry.rb +22 -3
  60. data/lib/tools/remember.rb +179 -0
  61. data/lib/tools/request_feature.rb +3 -1
  62. data/lib/tools/spawn_specialist.rb +21 -9
  63. data/lib/tools/spawn_subagent.rb +22 -11
  64. data/lib/tools/subagent_prompts.rb +20 -3
  65. data/lib/tools/web_get.rb +21 -10
  66. data/lib/tui/app.rb +222 -125
  67. data/lib/tui/decorators/base_decorator.rb +165 -0
  68. data/lib/tui/decorators/bash_decorator.rb +20 -0
  69. data/lib/tui/decorators/edit_decorator.rb +19 -0
  70. data/lib/tui/decorators/read_decorator.rb +24 -0
  71. data/lib/tui/decorators/think_decorator.rb +36 -0
  72. data/lib/tui/decorators/web_get_decorator.rb +19 -0
  73. data/lib/tui/decorators/write_decorator.rb +19 -0
  74. data/lib/tui/flash.rb +139 -0
  75. data/lib/tui/formatting.rb +28 -0
  76. data/lib/tui/height_map.rb +93 -0
  77. data/lib/tui/message_store.rb +97 -8
  78. data/lib/tui/performance_logger.rb +90 -0
  79. data/lib/tui/screens/chat.rb +358 -133
  80. data/templates/config.toml +47 -0
  81. data/templates/soul.md +1 -1
  82. metadata +83 -4
  83. data/CHANGELOG.md +0 -80
  84. data/Gemfile +0 -17
  85. data/lib/tools/return_result.rb +0 -81
@@ -5,7 +5,12 @@ 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
- # Results are delivered back through {Tools::ReturnResult}.
8
+ # Nickname assignment is handled by the {AnalyticalBrain::Runner} which
9
+ # runs synchronously at spawn time, generating a unique nickname based
10
+ # on the task — same as generic sub-agents.
11
+ #
12
+ # Results are delivered through natural text messages routed by
13
+ # {Events::Subscribers::SubagentMessageRouter}.
9
14
  #
10
15
  # @see Agents::Registry
11
16
  # @see Agents::Definition
@@ -17,7 +22,9 @@ module Tools
17
22
  # Builds description dynamically to include available specialists.
18
23
  def self.description
19
24
  base = "Spawn a named specialist sub-agent to work on a task autonomously. " \
20
- "The specialist has a predefined role, system prompt, and tool set."
25
+ "The specialist has a predefined role, system prompt, and tool set. " \
26
+ "Its text messages are forwarded to you automatically. " \
27
+ "Address it via @name to send follow-up instructions."
21
28
 
22
29
  registry = Agents::Registry.instance
23
30
  return base unless registry.any?
@@ -34,7 +41,7 @@ module Tools
34
41
  name: name_property,
35
42
  task: {
36
43
  type: "string",
37
- description: "What the specialist should do (emitted as its first user message)"
44
+ description: "What the specialist should do (persisted as its first user message)"
38
45
  },
39
46
  expected_output: {
40
47
  type: "string",
@@ -66,7 +73,8 @@ module Tools
66
73
  end
67
74
 
68
75
  # Creates a child session with the specialist's predefined prompt and tools,
69
- # emits the task as a user message, and queues background processing.
76
+ # persists the task as a user message, and queues background processing.
77
+ # Returns immediately (non-blocking).
70
78
  #
71
79
  # @param input [Hash<String, Object>] with "name", "task", and "expected_output"
72
80
  # @return [String] confirmation with child session ID
@@ -84,7 +92,10 @@ module Tools
84
92
  return {error: "Unknown agent: #{name}"} unless definition
85
93
 
86
94
  child = spawn_child(definition, task, expected_output)
87
- "Specialist '#{name}' spawned (session #{child.id}). Result will arrive as a tool response."
95
+ nickname = child.name
96
+ "Specialist @#{nickname} spawned (session #{child.id}). " \
97
+ "Its messages will appear in your conversation. " \
98
+ "Reply with @#{nickname} to send it instructions."
88
99
  end
89
100
 
90
101
  private
@@ -94,16 +105,17 @@ module Tools
94
105
  child = Session.create!(
95
106
  parent_session_id: @session.id,
96
107
  prompt: prompt,
97
- granted_tools: definition.tools,
98
- name: definition.name
108
+ granted_tools: definition.tools
99
109
  )
100
- Events::Bus.emit(Events::UserMessage.new(content: task, session_id: child.id))
110
+ child.create_user_event(task)
111
+ assign_nickname_via_brain(child)
112
+ child.broadcast_children_update_to_parent
101
113
  AgentRequestJob.perform_later(child.id)
102
114
  child
103
115
  end
104
116
 
105
117
  def build_prompt(definition, expected_output)
106
- "#{definition.prompt}\n\n#{RETURN_INSTRUCTION}\n\n#{EXPECTED_DELIVERABLE_PREFIX}#{expected_output}"
118
+ "#{definition.prompt}\n\n#{COMMUNICATION_INSTRUCTION}\n\n#{EXPECTED_DELIVERABLE_PREFIX}#{expected_output}"
107
119
  end
108
120
  end
109
121
  end
@@ -3,21 +3,26 @@
3
3
  module Tools
4
4
  # Spawns a generic child session that works on a task autonomously.
5
5
  # The sub-agent inherits the parent's viewport context at fork time,
6
- # runs via {AgentRequestJob}, and delivers results back
7
- # through {Tools::ReturnResult}.
6
+ # runs via {AgentRequestJob}, and communicates with the parent through
7
+ # natural text messages routed by {Events::Subscribers::SubagentMessageRouter}.
8
+ #
9
+ # Nickname assignment is handled by the {AnalyticalBrain::Runner} which
10
+ # runs synchronously at spawn time — the same brain that manages skills,
11
+ # goals, and workflows for the main session.
8
12
  #
9
13
  # For named specialists with predefined prompts and tools, see {SpawnSpecialist}.
10
14
  class SpawnSubagent < Base
11
15
  include SubagentPrompts
12
16
 
13
- GENERIC_PROMPT = "You are a focused sub-agent. #{RETURN_INSTRUCTION}\n"
17
+ GENERIC_PROMPT = "You are a focused sub-agent. #{COMMUNICATION_INSTRUCTION}\n"
14
18
 
15
19
  def self.tool_name = "spawn_subagent"
16
20
 
17
21
  def self.description
18
22
  "Spawn a generic sub-agent to work on a task autonomously. " \
19
23
  "The sub-agent inherits your conversation context, works independently, " \
20
- "and returns results as a tool response when done."
24
+ "and its text messages are forwarded to you automatically. " \
25
+ "Address it via @nickname to send follow-up instructions."
21
26
  end
22
27
 
23
28
  def self.input_schema
@@ -26,7 +31,7 @@ module Tools
26
31
  properties: {
27
32
  task: {
28
33
  type: "string",
29
- description: "What the sub-agent should do (emitted as its first user message)"
34
+ description: "What the sub-agent should do (persisted as its first user message)"
30
35
  },
31
36
  expected_output: {
32
37
  type: "string",
@@ -36,7 +41,7 @@ module Tools
36
41
  type: "array",
37
42
  items: {type: "string"},
38
43
  description: "Tool names to grant the sub-agent. " \
39
- "Omit for all standard tools. Empty array for pure reasoning (return_result only). " \
44
+ "Omit for all standard tools. Empty array for pure reasoning. " \
40
45
  "Valid tools: #{AgentLoop::STANDARD_TOOLS_BY_NAME.keys.join(", ")}"
41
46
  }
42
47
  },
@@ -49,11 +54,12 @@ module Tools
49
54
  @session = session
50
55
  end
51
56
 
52
- # Creates a child session, emits the task as a user message, and
53
- # queues background processing. Returns immediately (non-blocking).
57
+ # Creates a child session, runs the analytical brain to assign a nickname,
58
+ # persists the task as a user message, and queues background processing.
59
+ # Returns immediately after brain completes (blocking for ~200ms).
54
60
  #
55
61
  # @param input [Hash<String, Object>] with "task", "expected_output", and optional "tools"
56
- # @return [String] confirmation with child session ID
62
+ # @return [String] confirmation with child session ID and @nickname
57
63
  # @return [Hash{Symbol => String}] with :error key on validation failure
58
64
  def execute(input)
59
65
  task = input["task"].to_s.strip
@@ -68,7 +74,10 @@ module Tools
68
74
  return error if error
69
75
 
70
76
  child = spawn_child(task, expected_output, tools)
71
- "Sub-agent spawned (session #{child.id}). Result will arrive as a tool response."
77
+ nickname = child.name
78
+ "Sub-agent @#{nickname} spawned (session #{child.id}). " \
79
+ "Its messages will appear in your conversation. " \
80
+ "Reply with @#{nickname} to send it instructions."
72
81
  end
73
82
 
74
83
  private
@@ -79,7 +88,9 @@ module Tools
79
88
  prompt: "#{GENERIC_PROMPT}\n#{EXPECTED_DELIVERABLE_PREFIX}#{expected_output}",
80
89
  granted_tools: granted_tools
81
90
  )
82
- Events::Bus.emit(Events::UserMessage.new(content: task, session_id: child.id))
91
+ child.create_user_event(task)
92
+ assign_nickname_via_brain(child)
93
+ child.broadcast_children_update_to_parent
83
94
  AgentRequestJob.perform_later(child.id)
84
95
  child
85
96
  end
@@ -1,12 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tools
4
- # Shared prompt fragments for tools that spawn sub-agent sessions.
4
+ # Shared prompt fragments and nickname logic for tools that spawn sub-agent sessions.
5
5
  # Included by {SpawnSubagent} and {SpawnSpecialist} to avoid duplication.
6
6
  module SubagentPrompts
7
- RETURN_INSTRUCTION = "Complete the assigned task, then call the return_result tool with your deliverable. " \
8
- "Do not ask follow-up questions work with the context you have."
7
+ COMMUNICATION_INSTRUCTION = "Your text messages are automatically forwarded to the parent agent. " \
8
+ "When you finish, write your final summary and stop no special tool needed. " \
9
+ "If you need clarification, just ask — the parent can reply."
9
10
 
10
11
  EXPECTED_DELIVERABLE_PREFIX = "Expected deliverable: "
12
+
13
+ private
14
+
15
+ # Runs the analytical brain synchronously to assign a nickname.
16
+ # Falls back to a sequential "agent-N" name on any failure.
17
+ def assign_nickname_via_brain(child)
18
+ AnalyticalBrain::Runner.new(child).call
19
+ child.reload
20
+ rescue => error
21
+ Rails.logger.warn("Sub-agent nickname assignment failed: #{error.message}")
22
+ child.update!(name: fallback_nickname)
23
+ end
24
+
25
+ def fallback_nickname
26
+ "agent-#{@session.child_sessions.count}"
27
+ end
11
28
  end
12
29
  end
data/lib/tools/web_get.rb CHANGED
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "certifi"
3
4
  require "httparty"
4
5
 
5
6
  module Tools
6
- # Fetches content from a URL via HTTP GET. Returns the response body
7
- # as plain text, truncated to {Anima::Settings.max_web_response_bytes} to prevent memory issues.
7
+ # Fetches content from a URL via HTTP GET. Returns a structured result with
8
+ # the response body and Content-Type header so that {ToolDecorator} can apply
9
+ # format-specific conversion (HTML → Markdown, JSON → TOON, etc.).
10
+ #
11
+ # The body is truncated to {Anima::Settings.max_web_response_bytes} before
12
+ # decoration to cap memory usage on large responses.
8
13
  #
9
14
  # Only http and https schemes are allowed.
10
15
  class WebGet < Base
@@ -22,24 +27,30 @@ module Tools
22
27
  }
23
28
  end
24
29
 
25
- # @param input [Hash<String, Object>] string-keyed hash from the Anthropic API
26
- # @return [String] response body (possibly truncated)
27
- # @return [Hash] with :error key on failure
30
+ # @param input [Hash<String, Object>] string-keyed hash from the Anthropic API.
31
+ # Supports optional "timeout" key (seconds) to override the global
32
+ # web_request_timeout setting.
33
+ # @return [Hash] `{body: String, content_type: String}` on success
34
+ # @return [Hash] `{error: String}` on failure
28
35
  def execute(input)
29
- validate_and_fetch(input["url"].to_s)
36
+ validate_and_fetch(input["url"].to_s, timeout: input["timeout"])
30
37
  end
31
38
 
32
39
  private
33
40
 
34
- def validate_and_fetch(url)
35
- timeout = Anima::Settings.web_request_timeout
41
+ def validate_and_fetch(url, timeout: nil)
42
+ timeout ||= Anima::Settings.web_request_timeout
36
43
  scheme = URI.parse(url).scheme
37
44
 
38
45
  unless %w[http https].include?(scheme)
39
46
  return {error: "Only http and https URLs are supported, got: #{scheme.inspect}"}
40
47
  end
41
48
 
42
- truncate_body(HTTParty.get(url, timeout: timeout, follow_redirects: false).body.to_s)
49
+ response = HTTParty.get(url, timeout: timeout, follow_redirects: false, ssl_ca_file: Certifi.where)
50
+ body = truncate_body(response.body.to_s)
51
+ content_type = response.content_type || "text/plain"
52
+
53
+ {body: body, content_type: content_type}
43
54
  rescue URI::InvalidURIError => error
44
55
  {error: "Invalid URL: #{error.message}"}
45
56
  rescue Net::OpenTimeout, Net::ReadTimeout
@@ -54,7 +65,7 @@ module Tools
54
65
  max_bytes = Anima::Settings.max_web_response_bytes
55
66
  return body if body.bytesize <= max_bytes
56
67
 
57
- body.byteslice(0, max_bytes) +
68
+ body.byteslice(0, max_bytes).scrub +
58
69
  "\n\n[Truncated: response exceeded #{max_bytes} bytes]"
59
70
  end
60
71
  end