anima-core 1.1.3 → 1.3.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 +10 -1
- data/README.md +36 -11
- data/agents/codebase-analyzer.md +2 -2
- data/agents/codebase-pattern-finder.md +2 -2
- data/agents/documentation-researcher.md +2 -2
- data/agents/thoughts-analyzer.md +2 -2
- data/agents/web-search-researcher.md +3 -3
- data/app/channels/session_channel.rb +83 -64
- 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 +6 -6
- data/app/decorators/tool_decorator.rb +4 -4
- data/app/decorators/tool_response_decorator.rb +2 -2
- data/app/decorators/user_message_decorator.rb +5 -19
- data/app/decorators/web_get_tool_decorator.rb +41 -9
- data/app/jobs/agent_request_job.rb +33 -24
- 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 +17 -4
- data/app/models/goal_pinned_message.rb +11 -0
- data/app/models/message.rb +127 -0
- data/app/models/pending_message.rb +43 -0
- data/app/models/pinned_message.rb +41 -0
- data/app/models/secret.rb +72 -0
- data/app/models/session.rb +385 -226
- data/app/models/snapshot.rb +25 -25
- data/config/environments/test.rb +5 -0
- data/config/initializers/time_nanoseconds.rb +11 -0
- data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
- data/db/migrate/20260328100000_create_secrets.rb +15 -0
- data/db/migrate/20260328152142_add_evicted_at_to_goals.rb +6 -0
- data/db/migrate/20260329120000_create_pending_messages.rb +11 -0
- data/lib/agent_loop.rb +14 -41
- data/lib/agents/definition.rb +1 -1
- data/lib/analytical_brain/runner.rb +40 -37
- 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/cli/mcp/secrets.rb +4 -4
- data/lib/anima/cli/mcp.rb +4 -4
- data/lib/anima/installer.rb +7 -1
- data/lib/anima/settings.rb +46 -6
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +1 -1
- data/lib/credential_store.rb +17 -66
- data/lib/events/base.rb +1 -1
- data/lib/events/bounce_back.rb +7 -7
- data/lib/events/subscribers/persister.rb +15 -22
- data/lib/events/subscribers/subagent_message_router.rb +20 -8
- data/lib/events/subscribers/transient_broadcaster.rb +2 -2
- data/lib/events/user_message.rb +2 -13
- data/lib/llm/client.rb +54 -20
- data/lib/mcp/config.rb +2 -2
- data/lib/mcp/secrets.rb +7 -8
- 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/shell_session.rb +54 -16
- data/lib/tools/base.rb +23 -0
- data/lib/tools/bash.rb +60 -16
- data/lib/tools/edit.rb +6 -8
- data/lib/tools/mark_goal_completed.rb +86 -0
- data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
- data/lib/tools/read.rb +6 -5
- data/lib/tools/recall.rb +98 -0
- data/lib/tools/registry.rb +37 -8
- data/lib/tools/remember.rb +46 -55
- data/lib/tools/response_truncator.rb +70 -0
- data/lib/tools/spawn_specialist.rb +15 -25
- data/lib/tools/spawn_subagent.rb +14 -22
- data/lib/tools/subagent_prompts.rb +42 -6
- data/lib/tools/think.rb +26 -10
- data/lib/tools/web_get.rb +23 -4
- data/lib/tools/write.rb +4 -4
- data/lib/tui/app.rb +178 -13
- data/lib/tui/braille_spinner.rb +152 -0
- data/lib/tui/cable_client.rb +4 -4
- data/lib/tui/decorators/base_decorator.rb +17 -8
- data/lib/tui/decorators/bash_decorator.rb +2 -2
- data/lib/tui/decorators/edit_decorator.rb +5 -4
- data/lib/tui/decorators/read_decorator.rb +4 -8
- data/lib/tui/decorators/think_decorator.rb +3 -5
- data/lib/tui/decorators/web_get_decorator.rb +4 -3
- data/lib/tui/decorators/write_decorator.rb +5 -4
- data/lib/tui/flash.rb +1 -1
- data/lib/tui/formatting.rb +22 -0
- data/lib/tui/message_store.rb +103 -59
- data/lib/tui/screens/chat.rb +293 -78
- 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 +42 -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 +20 -9
- data/app/jobs/count_event_tokens_job.rb +0 -39
- data/app/models/event.rb +0 -129
- 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
|
@@ -8,19 +8,15 @@ module AnalyticalBrain
|
|
|
8
8
|
class ActivateSkill < ::Tools::Base
|
|
9
9
|
def self.tool_name = "activate_skill"
|
|
10
10
|
|
|
11
|
-
def self.description = "
|
|
12
|
-
"The skill's content will be injected into the agent's system prompt."
|
|
11
|
+
def self.description = "Give the agent domain knowledge relevant to the current conversation."
|
|
13
12
|
|
|
14
13
|
def self.input_schema
|
|
15
14
|
{
|
|
16
15
|
type: "object",
|
|
17
16
|
properties: {
|
|
18
|
-
|
|
19
|
-
type: "string",
|
|
20
|
-
description: "Name of the skill to activate (from the available skills list)"
|
|
21
|
-
}
|
|
17
|
+
skill_name: {type: "string"}
|
|
22
18
|
},
|
|
23
|
-
required: %w[
|
|
19
|
+
required: %w[skill_name]
|
|
24
20
|
}
|
|
25
21
|
end
|
|
26
22
|
|
|
@@ -29,11 +25,11 @@ module AnalyticalBrain
|
|
|
29
25
|
@main_session = main_session
|
|
30
26
|
end
|
|
31
27
|
|
|
32
|
-
# @param input [Hash<String, Object>] with "
|
|
28
|
+
# @param input [Hash<String, Object>] with "skill_name" key
|
|
33
29
|
# @return [String] confirmation message with skill description
|
|
34
30
|
# @return [Hash] with :error key on validation failure
|
|
35
31
|
def execute(input)
|
|
36
|
-
skill_name = input["
|
|
32
|
+
skill_name = input["skill_name"].to_s.strip
|
|
37
33
|
return {error: "Skill name cannot be blank"} if skill_name.empty?
|
|
38
34
|
|
|
39
35
|
skill = @main_session.activate_skill(skill_name)
|
|
@@ -17,8 +17,7 @@ module AnalyticalBrain
|
|
|
17
17
|
|
|
18
18
|
def self.tool_name = "assign_nickname"
|
|
19
19
|
|
|
20
|
-
def self.description = "Assign a
|
|
21
|
-
"The nickname is permanent — it will not change."
|
|
20
|
+
def self.description = "Assign a permanent nickname to this sub-agent."
|
|
22
21
|
|
|
23
22
|
def self.input_schema
|
|
24
23
|
{
|
|
@@ -26,8 +25,7 @@ module AnalyticalBrain
|
|
|
26
25
|
properties: {
|
|
27
26
|
nickname: {
|
|
28
27
|
type: "string",
|
|
29
|
-
description: "
|
|
30
|
-
"Evocative of the task, fun, easy to type after @."
|
|
28
|
+
description: "Lowercase, hyphenated (e.g. 'loop-sleuth')."
|
|
31
29
|
}
|
|
32
30
|
},
|
|
33
31
|
required: %w[nickname]
|
|
@@ -7,19 +7,15 @@ module AnalyticalBrain
|
|
|
7
7
|
class DeactivateSkill < ::Tools::Base
|
|
8
8
|
def self.tool_name = "deactivate_skill"
|
|
9
9
|
|
|
10
|
-
def self.description = "
|
|
11
|
-
"The skill's content will be removed from the agent's system prompt."
|
|
10
|
+
def self.description = "Remove domain knowledge that is no longer relevant."
|
|
12
11
|
|
|
13
12
|
def self.input_schema
|
|
14
13
|
{
|
|
15
14
|
type: "object",
|
|
16
15
|
properties: {
|
|
17
|
-
|
|
18
|
-
type: "string",
|
|
19
|
-
description: "Name of the skill to deactivate (from the currently active skills list)"
|
|
20
|
-
}
|
|
16
|
+
skill_name: {type: "string"}
|
|
21
17
|
},
|
|
22
|
-
required: %w[
|
|
18
|
+
required: %w[skill_name]
|
|
23
19
|
}
|
|
24
20
|
end
|
|
25
21
|
|
|
@@ -28,11 +24,11 @@ module AnalyticalBrain
|
|
|
28
24
|
@main_session = main_session
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
# @param input [Hash<String, Object>] with "
|
|
27
|
+
# @param input [Hash<String, Object>] with "skill_name" key
|
|
32
28
|
# @return [String] confirmation message
|
|
33
29
|
# @return [Hash] with :error key on validation failure
|
|
34
30
|
def execute(input)
|
|
35
|
-
skill_name = input["
|
|
31
|
+
skill_name = input["skill_name"].to_s.strip
|
|
36
32
|
return {error: "Skill name cannot be blank"} if skill_name.empty?
|
|
37
33
|
|
|
38
34
|
@main_session.deactivate_skill(skill_name)
|
|
@@ -11,8 +11,7 @@ module AnalyticalBrain
|
|
|
11
11
|
class EverythingIsReady < ::Tools::Base
|
|
12
12
|
def self.tool_name = "everything_is_ready"
|
|
13
13
|
|
|
14
|
-
def self.description = "
|
|
15
|
-
"Call this when the session name and active skills are already appropriate."
|
|
14
|
+
def self.description = "Nothing else to do."
|
|
16
15
|
|
|
17
16
|
def self.input_schema
|
|
18
17
|
{type: "object", properties: {}, required: []}
|
|
@@ -7,17 +7,13 @@ module AnalyticalBrain
|
|
|
7
7
|
class FinishGoal < ::Tools::Base
|
|
8
8
|
def self.tool_name = "finish_goal"
|
|
9
9
|
|
|
10
|
-
def self.description = "Mark a goal as completed.
|
|
11
|
-
"Use this when the main agent has finished the work described by the goal."
|
|
10
|
+
def self.description = "Mark a goal as completed."
|
|
12
11
|
|
|
13
12
|
def self.input_schema
|
|
14
13
|
{
|
|
15
14
|
type: "object",
|
|
16
15
|
properties: {
|
|
17
|
-
goal_id: {
|
|
18
|
-
type: "integer",
|
|
19
|
-
description: "ID of the goal to mark as completed"
|
|
20
|
-
}
|
|
16
|
+
goal_id: {type: "integer"}
|
|
21
17
|
},
|
|
22
18
|
required: %w[goal_id]
|
|
23
19
|
}
|
|
@@ -49,7 +45,8 @@ module AnalyticalBrain
|
|
|
49
45
|
# brain learns to check status before retrying.
|
|
50
46
|
def complete(goal)
|
|
51
47
|
id = goal.id
|
|
52
|
-
|
|
48
|
+
desc = goal.description
|
|
49
|
+
return {error: "Goal already completed: #{desc} (id: #{id})"} if goal.completed?
|
|
53
50
|
|
|
54
51
|
released = 0
|
|
55
52
|
Goal.transaction do
|
|
@@ -58,7 +55,7 @@ module AnalyticalBrain
|
|
|
58
55
|
released = goal.release_orphaned_pins!
|
|
59
56
|
end
|
|
60
57
|
|
|
61
|
-
msg = "Goal completed: #{
|
|
58
|
+
msg = "Goal completed: #{desc} (id: #{id})"
|
|
62
59
|
msg += " (released #{released} orphaned pins)" if released > 0
|
|
63
60
|
msg
|
|
64
61
|
end
|
|
@@ -9,19 +9,15 @@ module AnalyticalBrain
|
|
|
9
9
|
class ReadWorkflow < ::Tools::Base
|
|
10
10
|
def self.tool_name = "read_workflow"
|
|
11
11
|
|
|
12
|
-
def self.description = "
|
|
13
|
-
"Use the content to create appropriate goals with set_goal."
|
|
12
|
+
def self.description = "Activate a workflow and return its content for goal planning."
|
|
14
13
|
|
|
15
14
|
def self.input_schema
|
|
16
15
|
{
|
|
17
16
|
type: "object",
|
|
18
17
|
properties: {
|
|
19
|
-
|
|
20
|
-
type: "string",
|
|
21
|
-
description: "Name of the workflow to read (from the available workflows list)"
|
|
22
|
-
}
|
|
18
|
+
workflow_name: {type: "string"}
|
|
23
19
|
},
|
|
24
|
-
required: %w[
|
|
20
|
+
required: %w[workflow_name]
|
|
25
21
|
}
|
|
26
22
|
end
|
|
27
23
|
|
|
@@ -30,11 +26,11 @@ module AnalyticalBrain
|
|
|
30
26
|
@main_session = main_session
|
|
31
27
|
end
|
|
32
28
|
|
|
33
|
-
# @param input [Hash<String, Object>] with "
|
|
29
|
+
# @param input [Hash<String, Object>] with "workflow_name" key
|
|
34
30
|
# @return [String] workflow name, description, and full content
|
|
35
31
|
# @return [Hash] with :error key on validation failure
|
|
36
32
|
def execute(input)
|
|
37
|
-
workflow_name = input["
|
|
33
|
+
workflow_name = input["workflow_name"].to_s.strip
|
|
38
34
|
return {error: "Workflow name cannot be blank"} if workflow_name.empty?
|
|
39
35
|
|
|
40
36
|
workflow = @main_session.activate_workflow(workflow_name)
|
|
@@ -11,21 +11,14 @@ module AnalyticalBrain
|
|
|
11
11
|
class RenameSession < ::Tools::Base
|
|
12
12
|
def self.tool_name = "rename_session"
|
|
13
13
|
|
|
14
|
-
def self.description = "Rename the
|
|
15
|
-
"Use one emoji followed by 1-3 descriptive words."
|
|
14
|
+
def self.description = "Rename the session."
|
|
16
15
|
|
|
17
16
|
def self.input_schema
|
|
18
17
|
{
|
|
19
18
|
type: "object",
|
|
20
19
|
properties: {
|
|
21
|
-
emoji: {
|
|
22
|
-
|
|
23
|
-
description: "A single emoji representing the conversation topic"
|
|
24
|
-
},
|
|
25
|
-
name: {
|
|
26
|
-
type: "string",
|
|
27
|
-
description: "1-3 word descriptive name for the session"
|
|
28
|
-
}
|
|
20
|
+
emoji: {type: "string"},
|
|
21
|
+
name: {type: "string", description: "1-3 words."}
|
|
29
22
|
},
|
|
30
23
|
required: %w[emoji name]
|
|
31
24
|
}
|
|
@@ -8,8 +8,7 @@ module AnalyticalBrain
|
|
|
8
8
|
class SetGoal < ::Tools::Base
|
|
9
9
|
def self.tool_name = "set_goal"
|
|
10
10
|
|
|
11
|
-
def self.description = "Create a goal
|
|
12
|
-
"Omit parent_goal_id for a root goal, or provide it to create a sub-goal (TODO item)."
|
|
11
|
+
def self.description = "Create a goal or sub-goal."
|
|
13
12
|
|
|
14
13
|
def self.input_schema
|
|
15
14
|
{
|
|
@@ -17,12 +16,9 @@ module AnalyticalBrain
|
|
|
17
16
|
properties: {
|
|
18
17
|
description: {
|
|
19
18
|
type: "string",
|
|
20
|
-
description: "
|
|
19
|
+
description: "1 sentence."
|
|
21
20
|
},
|
|
22
|
-
parent_goal_id: {
|
|
23
|
-
type: "integer",
|
|
24
|
-
description: "ID of the parent goal (omit for root goals)"
|
|
25
|
-
}
|
|
21
|
+
parent_goal_id: {type: "integer"}
|
|
26
22
|
},
|
|
27
23
|
required: %w[description]
|
|
28
24
|
}
|
|
@@ -15,20 +15,16 @@ module AnalyticalBrain
|
|
|
15
15
|
class UpdateGoal < ::Tools::Base
|
|
16
16
|
def self.tool_name = "update_goal"
|
|
17
17
|
|
|
18
|
-
def self.description = "
|
|
19
|
-
"Use this to refine a goal as understanding evolves."
|
|
18
|
+
def self.description = "Refine a goal's wording as understanding evolves."
|
|
20
19
|
|
|
21
20
|
def self.input_schema
|
|
22
21
|
{
|
|
23
22
|
type: "object",
|
|
24
23
|
properties: {
|
|
25
|
-
goal_id: {
|
|
26
|
-
type: "integer",
|
|
27
|
-
description: "ID of the goal to update"
|
|
28
|
-
},
|
|
24
|
+
goal_id: {type: "integer"},
|
|
29
25
|
description: {
|
|
30
26
|
type: "string",
|
|
31
|
-
description: "
|
|
27
|
+
description: "1 sentence."
|
|
32
28
|
}
|
|
33
29
|
},
|
|
34
30
|
required: %w[goal_id description]
|
|
@@ -5,8 +5,8 @@ require "thor"
|
|
|
5
5
|
module Anima
|
|
6
6
|
class CLI < Thor
|
|
7
7
|
class Mcp < Thor
|
|
8
|
-
# CLI commands for managing MCP secrets stored in
|
|
9
|
-
#
|
|
8
|
+
# CLI commands for managing MCP secrets stored in the encrypted
|
|
9
|
+
# secrets table. Secrets are referenced in mcp.toml via
|
|
10
10
|
# +${credential:key_name}+ syntax.
|
|
11
11
|
#
|
|
12
12
|
# @example Store a secret
|
|
@@ -22,7 +22,7 @@ module Anima
|
|
|
22
22
|
true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
desc "set KEY=VALUE", "Store an MCP secret in encrypted
|
|
25
|
+
desc "set KEY=VALUE", "Store an MCP secret in encrypted secrets"
|
|
26
26
|
def set(pair)
|
|
27
27
|
key, value = pair.split("=", 2)
|
|
28
28
|
unless value
|
|
@@ -50,7 +50,7 @@ module Anima
|
|
|
50
50
|
keys.each { |key| say " #{key}" }
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
desc "remove KEY", "Remove an MCP secret from encrypted
|
|
53
|
+
desc "remove KEY", "Remove an MCP secret from encrypted storage"
|
|
54
54
|
def remove(key)
|
|
55
55
|
secrets = require_mcp_secrets
|
|
56
56
|
unless secrets.list.include?(key)
|
data/lib/anima/cli/mcp.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Anima
|
|
|
12
12
|
true
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
desc "secrets SUBCOMMAND", "Manage MCP secrets in encrypted
|
|
15
|
+
desc "secrets SUBCOMMAND", "Manage MCP secrets in encrypted storage"
|
|
16
16
|
subcommand "secrets", Secrets
|
|
17
17
|
|
|
18
18
|
desc "list", "List configured MCP servers with health status"
|
|
@@ -40,14 +40,14 @@ module Anima
|
|
|
40
40
|
|
|
41
41
|
Use -e KEY=VALUE to set environment variables (stdio servers).
|
|
42
42
|
Use -H "Header: Value" to set HTTP headers (HTTP servers).
|
|
43
|
-
Use -s KEY=VALUE to store a secret in encrypted
|
|
43
|
+
Use -s KEY=VALUE to store a secret in encrypted storage.
|
|
44
44
|
DESC
|
|
45
45
|
option :env, aliases: "-e", type: :string, repeatable: true, banner: "KEY=VALUE",
|
|
46
46
|
desc: "Environment variables (repeatable)"
|
|
47
47
|
option :header, aliases: "-H", type: :string, repeatable: true, banner: "Header: Value",
|
|
48
48
|
desc: "HTTP headers (repeatable)"
|
|
49
49
|
option :secret, aliases: "-s", type: :string, repeatable: true, banner: "KEY=VALUE",
|
|
50
|
-
desc: "Store secret in encrypted
|
|
50
|
+
desc: "Store secret in encrypted storage (repeatable)"
|
|
51
51
|
def add(name, *rest)
|
|
52
52
|
if rest.empty?
|
|
53
53
|
say "Error: missing server URL or command.", :red
|
|
@@ -87,7 +87,7 @@ module Anima
|
|
|
87
87
|
::Mcp::Config.new
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
# Stores secrets from -s KEY=VALUE flags in encrypted
|
|
90
|
+
# Stores secrets from -s KEY=VALUE flags in the encrypted secrets table.
|
|
91
91
|
def store_secrets(secret_strings)
|
|
92
92
|
return unless secret_strings&.any?
|
|
93
93
|
|
data/lib/anima/installer.rb
CHANGED
|
@@ -117,7 +117,13 @@ module Anima
|
|
|
117
117
|
raise_if_missing_key: true
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
-
config.write(
|
|
120
|
+
config.write(<<~YAML)
|
|
121
|
+
secret_key_base: #{SecureRandom.hex(64)}
|
|
122
|
+
active_record_encryption:
|
|
123
|
+
primary_key: #{SecureRandom.base64(32)}
|
|
124
|
+
deterministic_key: #{SecureRandom.base64(32)}
|
|
125
|
+
key_derivation_salt: #{SecureRandom.base64(32)}
|
|
126
|
+
YAML
|
|
121
127
|
File.chmod(0o600, content_str)
|
|
122
128
|
say " created credentials for #{env}"
|
|
123
129
|
end
|
data/lib/anima/settings.rb
CHANGED
|
@@ -60,6 +60,13 @@ module Anima
|
|
|
60
60
|
self.config_path = nil
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# ─── Agent Identity ─────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
# The agent's display name. Separates engine identity ("Anima") from
|
|
66
|
+
# agent identity — any agent running on Anima can name itself.
|
|
67
|
+
# @return [String]
|
|
68
|
+
def agent_name = get("agent", "name")
|
|
69
|
+
|
|
63
70
|
# ─── LLM ───────────────────────────────────────────────────────
|
|
64
71
|
|
|
65
72
|
# Primary model for conversations.
|
|
@@ -84,6 +91,11 @@ module Anima
|
|
|
84
91
|
# @return [Integer]
|
|
85
92
|
def token_budget = get("llm", "token_budget")
|
|
86
93
|
|
|
94
|
+
# Maximum character length for the Think tool's thoughts parameter.
|
|
95
|
+
# Sub-agents receive half this budget (their tasks are less complex).
|
|
96
|
+
# @return [Integer]
|
|
97
|
+
def thinking_budget = get("llm", "thinking_budget")
|
|
98
|
+
|
|
87
99
|
# ─── Timeouts (seconds) ────────────────────────────────────────
|
|
88
100
|
|
|
89
101
|
# LLM API request timeout.
|
|
@@ -107,6 +119,13 @@ module Anima
|
|
|
107
119
|
# @return [Integer] seconds
|
|
108
120
|
def tool_timeout = get("timeouts", "tool")
|
|
109
121
|
|
|
122
|
+
# Polling interval for user interrupt checks during long-running commands.
|
|
123
|
+
# Enforces a 0.5s floor to prevent busy-polling from misconfiguration.
|
|
124
|
+
# @return [Numeric] seconds (minimum 0.5)
|
|
125
|
+
def interrupt_check_interval
|
|
126
|
+
[get("timeouts", "interrupt_check"), 0.5].max
|
|
127
|
+
end
|
|
128
|
+
|
|
110
129
|
# ─── Shell ──────────────────────────────────────────────────────
|
|
111
130
|
|
|
112
131
|
# Maximum bytes of command output before truncation.
|
|
@@ -119,11 +138,11 @@ module Anima
|
|
|
119
138
|
# @return [Integer]
|
|
120
139
|
def max_file_size = get("tools", "max_file_size")
|
|
121
140
|
|
|
122
|
-
# Maximum lines returned by the
|
|
141
|
+
# Maximum lines returned by the read_file tool.
|
|
123
142
|
# @return [Integer]
|
|
124
143
|
def max_read_lines = get("tools", "max_read_lines")
|
|
125
144
|
|
|
126
|
-
# Maximum bytes returned by the
|
|
145
|
+
# Maximum bytes returned by the read_file tool.
|
|
127
146
|
# @return [Integer]
|
|
128
147
|
def max_read_bytes = get("tools", "max_read_bytes")
|
|
129
148
|
|
|
@@ -131,6 +150,20 @@ module Anima
|
|
|
131
150
|
# @return [Integer]
|
|
132
151
|
def max_web_response_bytes = get("tools", "max_web_response_bytes")
|
|
133
152
|
|
|
153
|
+
# Minimum characters of extracted web content before flagging as possibly incomplete.
|
|
154
|
+
# @return [Integer]
|
|
155
|
+
def min_web_content_chars = get("tools", "min_web_content_chars")
|
|
156
|
+
|
|
157
|
+
# Maximum characters of tool output before head+tail truncation.
|
|
158
|
+
# Full output saved to a temp file for paginated reading.
|
|
159
|
+
# @return [Integer]
|
|
160
|
+
def max_tool_response_chars = get("tools", "max_tool_response_chars")
|
|
161
|
+
|
|
162
|
+
# Maximum characters of sub-agent result before head+tail truncation.
|
|
163
|
+
# Higher than tool threshold because sub-agent output is already curated.
|
|
164
|
+
# @return [Integer]
|
|
165
|
+
def max_subagent_response_chars = get("tools", "max_subagent_response_chars")
|
|
166
|
+
|
|
134
167
|
# ─── Session ────────────────────────────────────────────────────
|
|
135
168
|
|
|
136
169
|
# View mode applied to new sessions: "basic", "verbose", or "debug".
|
|
@@ -191,9 +224,16 @@ module Anima
|
|
|
191
224
|
# @return [Boolean]
|
|
192
225
|
def analytical_brain_blocking_on_agent_message = get("analytical_brain", "blocking_on_agent_message")
|
|
193
226
|
|
|
194
|
-
# Number of recent
|
|
227
|
+
# Number of recent messages to include in the analytical brain's context window.
|
|
228
|
+
# @return [Integer]
|
|
229
|
+
def analytical_brain_message_window = get("analytical_brain", "message_window")
|
|
230
|
+
|
|
231
|
+
# ─── Goals ──────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
# Number of meaningful messages (user + agent turns) after completion
|
|
234
|
+
# before a completed goal is automatically evicted from context.
|
|
195
235
|
# @return [Integer]
|
|
196
|
-
def
|
|
236
|
+
def completed_decay_messages = get("goals", "completed_decay_messages")
|
|
197
237
|
|
|
198
238
|
# ─── Mneme (Memory Department) ────────────────────────────────
|
|
199
239
|
|
|
@@ -217,8 +257,8 @@ module Anima
|
|
|
217
257
|
# @return [Integer]
|
|
218
258
|
def mneme_l2_snapshot_threshold = get("mneme", "l2_snapshot_threshold")
|
|
219
259
|
|
|
220
|
-
# Fraction of the main viewport token budget reserved for pinned
|
|
221
|
-
# Pinned
|
|
260
|
+
# Fraction of the main viewport token budget reserved for pinned messages.
|
|
261
|
+
# Pinned messages appear between snapshots and the sliding window.
|
|
222
262
|
# @return [Float]
|
|
223
263
|
def mneme_pinned_budget_fraction = get("mneme", "pinned_budget_fraction")
|
|
224
264
|
|
data/lib/anima/version.rb
CHANGED
data/lib/anima.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Anima
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# Boots Rails when CLI commands need access to Rails-managed resources
|
|
15
|
-
# like encrypted
|
|
15
|
+
# like the encrypted secrets table. No-op if Rails is already loaded.
|
|
16
16
|
def self.boot_rails!
|
|
17
17
|
return if defined?(Rails)
|
|
18
18
|
|
data/lib/credential_store.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
3
|
+
# Read/write operations for runtime secrets (API tokens, MCP credentials).
|
|
4
|
+
# Backed by the {Secret} model with Active Record Encryption — values are
|
|
5
|
+
# encrypted at rest and always fresh (no caching, no file-path issues in
|
|
6
|
+
# forked Solid Queue workers).
|
|
7
|
+
#
|
|
8
|
+
# All namespacing (e.g. +mcp+, +anthropic+) is the caller's responsibility.
|
|
7
9
|
#
|
|
8
10
|
# @example Writing a nested credential
|
|
9
11
|
# CredentialStore.write("mcp", "linear_api_key" => "sk-xxx")
|
|
@@ -13,91 +15,40 @@
|
|
|
13
15
|
class CredentialStore
|
|
14
16
|
class << self
|
|
15
17
|
# Writes one or more key-value pairs under a top-level namespace.
|
|
16
|
-
#
|
|
18
|
+
# Upserts: existing keys are updated, new keys are created.
|
|
17
19
|
#
|
|
18
|
-
# @param namespace [String] top-level
|
|
20
|
+
# @param namespace [String] top-level grouping key (e.g. "mcp", "anthropic")
|
|
19
21
|
# @param pairs [Hash<String, String>] key-value pairs to store
|
|
20
22
|
# @return [void]
|
|
21
23
|
def write(namespace, pairs)
|
|
22
|
-
|
|
23
|
-
section = existing[namespace] ||= {}
|
|
24
|
-
section.merge!(pairs)
|
|
25
|
-
save_credentials(existing)
|
|
24
|
+
Secret.write(namespace, pairs)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
# Reads a single credential value from a namespace.
|
|
29
|
-
# Busts the Rails credentials cache first so cross-process writes
|
|
30
|
-
# (e.g. token saved in the web process, read in the SolidQueue worker)
|
|
31
|
-
# are always visible.
|
|
32
28
|
#
|
|
33
|
-
# @param namespace [String] top-level
|
|
29
|
+
# @param namespace [String] top-level grouping key
|
|
34
30
|
# @param key [String] credential key within the namespace
|
|
35
31
|
# @return [String, nil] credential value or nil if not found
|
|
36
32
|
def read(namespace, key)
|
|
37
|
-
|
|
38
|
-
Rails.application.credentials.dig(namespace.to_sym, key.to_sym)
|
|
33
|
+
Secret.read(namespace, key)
|
|
39
34
|
end
|
|
40
35
|
|
|
41
|
-
# Lists all keys under a namespace.
|
|
36
|
+
# Lists all keys under a namespace (not values).
|
|
42
37
|
#
|
|
43
|
-
# @param namespace [String] top-level
|
|
44
|
-
# @return [Array<String>] credential keys
|
|
38
|
+
# @param namespace [String] top-level grouping key
|
|
39
|
+
# @return [Array<String>] credential keys
|
|
45
40
|
def list(namespace)
|
|
46
|
-
|
|
47
|
-
return [] unless section.is_a?(Hash)
|
|
48
|
-
|
|
49
|
-
section.keys.map(&:to_s)
|
|
41
|
+
Secret.list(namespace)
|
|
50
42
|
end
|
|
51
43
|
|
|
52
44
|
# Removes a single key from a namespace.
|
|
53
45
|
# No-op if the key does not exist.
|
|
54
46
|
#
|
|
55
|
-
# @param namespace [String] top-level
|
|
47
|
+
# @param namespace [String] top-level grouping key
|
|
56
48
|
# @param key [String] credential key to remove
|
|
57
49
|
# @return [void]
|
|
58
50
|
def remove(namespace, key)
|
|
59
|
-
|
|
60
|
-
section = existing[namespace]
|
|
61
|
-
return unless section.is_a?(Hash)
|
|
62
|
-
return unless section.key?(key)
|
|
63
|
-
|
|
64
|
-
section.delete(key)
|
|
65
|
-
existing.delete(namespace) if section.empty?
|
|
66
|
-
save_credentials(existing)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
# Reads and parses the raw YAML from encrypted credentials.
|
|
72
|
-
# Returns an empty hash when the credentials file does not exist yet.
|
|
73
|
-
#
|
|
74
|
-
# @return [Hash] parsed credentials
|
|
75
|
-
def load_credentials
|
|
76
|
-
creds = Rails.application.credentials
|
|
77
|
-
YAML.safe_load(creds.read) || {}
|
|
78
|
-
rescue ActiveSupport::EncryptedFile::MissingContentError
|
|
79
|
-
{}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Writes the full credentials hash back to the encrypted file and
|
|
83
|
-
# invalidates the Rails memoization cache so subsequent reads see
|
|
84
|
-
# fresh data.
|
|
85
|
-
#
|
|
86
|
-
# @param data [Hash] complete credentials hash to persist
|
|
87
|
-
# @return [void]
|
|
88
|
-
def save_credentials(data)
|
|
89
|
-
creds = Rails.application.credentials
|
|
90
|
-
creds.write(data.to_yaml)
|
|
91
|
-
bust_credentials_cache!
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Clears the Rails credentials in-memory cache so the next access
|
|
95
|
-
# re-reads and decrypts from disk. Required because Rails memoizes
|
|
96
|
-
# the decrypted config in @config per-process.
|
|
97
|
-
#
|
|
98
|
-
# @return [void]
|
|
99
|
-
def bust_credentials_cache!
|
|
100
|
-
Rails.application.credentials.instance_variable_set(:@config, nil)
|
|
51
|
+
Secret.remove(namespace, key)
|
|
101
52
|
end
|
|
102
53
|
end
|
|
103
54
|
end
|
data/lib/events/base.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Events
|
|
|
16
16
|
def initialize(content:, session_id: nil)
|
|
17
17
|
@content = content
|
|
18
18
|
@session_id = session_id
|
|
19
|
-
@timestamp =
|
|
19
|
+
@timestamp = Time.current.to_ns
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# @return [String] event type identifier
|
data/lib/events/bounce_back.rb
CHANGED
|
@@ -6,24 +6,24 @@ module Events
|
|
|
6
6
|
# this event notifies clients to remove the phantom message and
|
|
7
7
|
# restore the text to the input field.
|
|
8
8
|
#
|
|
9
|
-
# Not persisted — not included in {
|
|
9
|
+
# Not persisted — not included in {Message::TYPES}.
|
|
10
10
|
class BounceBack < Base
|
|
11
11
|
TYPE = "bounce_back"
|
|
12
12
|
|
|
13
13
|
# @return [String] human-readable error description
|
|
14
14
|
attr_reader :error
|
|
15
15
|
|
|
16
|
-
# @return [Integer, nil] database ID of the rolled-back
|
|
17
|
-
attr_reader :
|
|
16
|
+
# @return [Integer, nil] database ID of the rolled-back message (for client-side removal)
|
|
17
|
+
attr_reader :message_id
|
|
18
18
|
|
|
19
19
|
# @param content [String] original user message text to restore to input
|
|
20
20
|
# @param error [String] error description for the flash message
|
|
21
21
|
# @param session_id [Integer] session the message was intended for
|
|
22
|
-
# @param
|
|
23
|
-
def initialize(content:, error:, session_id:,
|
|
22
|
+
# @param message_id [Integer, nil] ID of the message that was broadcast optimistically
|
|
23
|
+
def initialize(content:, error:, session_id:, message_id: nil)
|
|
24
24
|
super(content: content, session_id: session_id)
|
|
25
25
|
@error = error
|
|
26
|
-
@
|
|
26
|
+
@message_id = message_id
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def type
|
|
@@ -31,7 +31,7 @@ module Events
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def to_h
|
|
34
|
-
super.merge(error: error,
|
|
34
|
+
super.merge(error: error, message_id: message_id)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|