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.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/.reek.yml +51 -0
- data/README.md +63 -29
- data/anima-core.gemspec +4 -1
- data/app/channels/session_channel.rb +30 -11
- data/app/decorators/tool_call_decorator.rb +32 -3
- data/app/decorators/tool_decorator.rb +57 -0
- data/app/decorators/tool_response_decorator.rb +12 -4
- data/app/decorators/web_get_tool_decorator.rb +102 -0
- data/app/jobs/agent_request_job.rb +93 -23
- data/app/jobs/mneme_job.rb +51 -0
- data/app/jobs/passive_recall_job.rb +29 -0
- data/app/models/concerns/event/broadcasting.rb +4 -0
- data/app/models/event.rb +10 -0
- data/app/models/goal.rb +27 -0
- data/app/models/goal_pinned_event.rb +11 -0
- data/app/models/pinned_event.rb +41 -0
- data/app/models/session.rb +402 -6
- data/app/models/snapshot.rb +76 -0
- data/bin/jobs +5 -0
- data/config/initializers/event_subscribers.rb +12 -3
- data/config/initializers/fts5_schema_dump.rb +21 -0
- data/config/queue.yml +0 -1
- data/db/migrate/20260321080000_create_mneme_schema.rb +32 -0
- data/db/migrate/20260321120000_create_pinned_events.rb +27 -0
- data/db/migrate/20260321140000_create_events_fts_index.rb +77 -0
- data/db/migrate/20260321140100_add_recalled_event_ids_to_sessions.rb +10 -0
- data/lib/agent_loop.rb +63 -20
- data/lib/analytical_brain/runner.rb +158 -65
- data/lib/analytical_brain/tools/assign_nickname.rb +76 -0
- data/lib/analytical_brain/tools/finish_goal.rb +6 -1
- data/lib/anima/cli.rb +32 -9
- data/lib/anima/installer.rb +11 -24
- data/lib/anima/settings.rb +59 -0
- data/lib/anima/spinner.rb +75 -0
- data/lib/anima/version.rb +1 -1
- data/lib/environment_probe.rb +4 -4
- data/lib/events/bounce_back.rb +37 -0
- data/lib/events/subscribers/persister.rb +19 -0
- data/lib/events/subscribers/subagent_message_router.rb +102 -0
- data/lib/events/subscribers/transient_broadcaster.rb +36 -0
- data/lib/events/tool_call.rb +5 -3
- data/lib/llm/client.rb +19 -9
- data/lib/mneme/compressed_viewport.rb +200 -0
- data/lib/mneme/l2_runner.rb +138 -0
- data/lib/mneme/passive_recall.rb +69 -0
- data/lib/mneme/runner.rb +254 -0
- data/lib/mneme/search.rb +150 -0
- data/lib/mneme/tools/attach_events_to_goals.rb +107 -0
- data/lib/mneme/tools/everything_ok.rb +24 -0
- data/lib/mneme/tools/save_snapshot.rb +68 -0
- data/lib/mneme.rb +29 -0
- data/lib/providers/anthropic.rb +57 -13
- data/lib/shell_session.rb +194 -63
- data/lib/tasks/fts5.rake +6 -0
- data/lib/tools/base.rb +2 -1
- data/lib/tools/bash.rb +4 -2
- data/lib/tools/registry.rb +22 -3
- data/lib/tools/remember.rb +179 -0
- data/lib/tools/request_feature.rb +3 -1
- data/lib/tools/spawn_specialist.rb +21 -9
- data/lib/tools/spawn_subagent.rb +22 -11
- data/lib/tools/subagent_prompts.rb +20 -3
- data/lib/tools/web_get.rb +21 -10
- data/lib/tui/app.rb +222 -125
- data/lib/tui/decorators/base_decorator.rb +165 -0
- data/lib/tui/decorators/bash_decorator.rb +20 -0
- data/lib/tui/decorators/edit_decorator.rb +19 -0
- data/lib/tui/decorators/read_decorator.rb +24 -0
- data/lib/tui/decorators/think_decorator.rb +36 -0
- data/lib/tui/decorators/web_get_decorator.rb +19 -0
- data/lib/tui/decorators/write_decorator.rb +19 -0
- data/lib/tui/flash.rb +139 -0
- data/lib/tui/formatting.rb +28 -0
- data/lib/tui/height_map.rb +93 -0
- data/lib/tui/message_store.rb +97 -8
- data/lib/tui/performance_logger.rb +90 -0
- data/lib/tui/screens/chat.rb +358 -133
- data/templates/config.toml +47 -0
- data/templates/soul.md +1 -1
- metadata +83 -4
- data/CHANGELOG.md +0 -80
- data/Gemfile +0 -17
- 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
|
-
#
|
|
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 (
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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#{
|
|
118
|
+
"#{definition.prompt}\n\n#{COMMUNICATION_INSTRUCTION}\n\n#{EXPECTED_DELIVERABLE_PREFIX}#{expected_output}"
|
|
107
119
|
end
|
|
108
120
|
end
|
|
109
121
|
end
|
data/lib/tools/spawn_subagent.rb
CHANGED
|
@@ -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
|
|
7
|
-
#
|
|
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. #{
|
|
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
|
|
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 (
|
|
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
|
|
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,
|
|
53
|
-
# queues background processing.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
"
|
|
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
|
|
7
|
-
#
|
|
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
|
-
#
|
|
27
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|