anima-core 0.2.1 → 1.0.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 +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Agents
|
|
6
|
+
class InvalidDefinitionError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# A named sub-agent parsed from a Markdown definition file.
|
|
9
|
+
# YAML frontmatter holds metadata; the Markdown body is the system prompt.
|
|
10
|
+
#
|
|
11
|
+
# @example Definition file format
|
|
12
|
+
# ---
|
|
13
|
+
# name: codebase-analyzer
|
|
14
|
+
# description: Analyzes codebase implementation details.
|
|
15
|
+
# tools: read, bash
|
|
16
|
+
# model: claude-sonnet-4-5
|
|
17
|
+
# ---
|
|
18
|
+
#
|
|
19
|
+
# You are a specialist at understanding HOW code works...
|
|
20
|
+
class Definition
|
|
21
|
+
# @return [String] unique agent identifier used in spawn_specialist(name: "...")
|
|
22
|
+
attr_reader :name
|
|
23
|
+
|
|
24
|
+
# @return [String] description shown to the LLM in the tool catalog
|
|
25
|
+
attr_reader :description
|
|
26
|
+
|
|
27
|
+
# @return [Array<String>] tool names available to this agent
|
|
28
|
+
attr_reader :tools
|
|
29
|
+
|
|
30
|
+
# @return [String] system prompt (Markdown body of the definition file)
|
|
31
|
+
attr_reader :prompt
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] LLM model override (reserved for future use)
|
|
34
|
+
attr_reader :model
|
|
35
|
+
|
|
36
|
+
# @return [String, nil] TUI display color (reserved for future use)
|
|
37
|
+
attr_reader :color
|
|
38
|
+
|
|
39
|
+
# @return [Integer, nil] maximum conversation turns (reserved for future use)
|
|
40
|
+
attr_reader :max_turns
|
|
41
|
+
|
|
42
|
+
# @return [String] file path this definition was loaded from
|
|
43
|
+
attr_reader :source_path
|
|
44
|
+
|
|
45
|
+
def initialize(name:, description:, tools:, prompt:, model: nil, color: nil, max_turns: nil, source_path: "")
|
|
46
|
+
@name = name
|
|
47
|
+
@description = description
|
|
48
|
+
@tools = tools
|
|
49
|
+
@prompt = prompt
|
|
50
|
+
@model = model
|
|
51
|
+
@color = color
|
|
52
|
+
@max_turns = max_turns
|
|
53
|
+
@source_path = source_path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Parses a Markdown file with YAML frontmatter into a Definition.
|
|
57
|
+
#
|
|
58
|
+
# @param path [String, Pathname] path to the .md file
|
|
59
|
+
# @return [Definition]
|
|
60
|
+
# @raise [InvalidDefinitionError] if required fields are missing or frontmatter is malformed
|
|
61
|
+
def self.from_file(path)
|
|
62
|
+
content = File.read(path)
|
|
63
|
+
frontmatter, body = parse_frontmatter(content)
|
|
64
|
+
|
|
65
|
+
validate_required_fields!(frontmatter, path)
|
|
66
|
+
|
|
67
|
+
new(
|
|
68
|
+
name: frontmatter["name"].to_s.strip,
|
|
69
|
+
description: frontmatter["description"].to_s.strip,
|
|
70
|
+
tools: parse_tools(frontmatter["tools"]),
|
|
71
|
+
prompt: body.strip,
|
|
72
|
+
model: frontmatter["model"]&.to_s&.strip,
|
|
73
|
+
color: frontmatter["color"]&.to_s&.strip,
|
|
74
|
+
max_turns: frontmatter["maxTurns"]&.to_i,
|
|
75
|
+
source_path: path.to_s
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param content [String] raw file content with YAML frontmatter
|
|
80
|
+
# @return [Array(Hash, String)] parsed frontmatter and body text
|
|
81
|
+
# @raise [InvalidDefinitionError] if frontmatter is missing or malformed
|
|
82
|
+
def self.parse_frontmatter(content)
|
|
83
|
+
# Opening "---" must be followed by a newline (not just whitespace).
|
|
84
|
+
# Non-greedy (.*?\n) captures YAML lines up to the closing "---".
|
|
85
|
+
# Closing "---" may optionally be followed by a newline before the body.
|
|
86
|
+
# The /m flag lets (.*) in the body capture across newlines.
|
|
87
|
+
match = content.match(/\A---\s*\n(.*?\n)---\s*\n?(.*)\z/m)
|
|
88
|
+
raise InvalidDefinitionError, "Missing YAML frontmatter" unless match
|
|
89
|
+
|
|
90
|
+
frontmatter = YAML.safe_load(match[1])
|
|
91
|
+
raise InvalidDefinitionError, "Frontmatter is not a valid YAML mapping" unless frontmatter.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
[frontmatter, match[2]]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Accepts comma-separated string or array of tool names.
|
|
97
|
+
#
|
|
98
|
+
# @param tools_value [String, Array, nil] raw tools field from frontmatter
|
|
99
|
+
# @return [Array<String>] normalized lowercase tool names
|
|
100
|
+
def self.parse_tools(tools_value)
|
|
101
|
+
return [] if tools_value.nil?
|
|
102
|
+
|
|
103
|
+
names = tools_value.is_a?(Array) ? tools_value : tools_value.to_s.split(",")
|
|
104
|
+
names.map { |tool| tool.to_s.strip.downcase }.reject(&:empty?).uniq
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.validate_required_fields!(frontmatter, path)
|
|
108
|
+
%w[name description].each do |field|
|
|
109
|
+
value = frontmatter[field].to_s.strip
|
|
110
|
+
raise InvalidDefinitionError, "Missing required field '#{field}' in #{path}" if value.empty?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private_class_method :parse_frontmatter, :parse_tools, :validate_required_fields!
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agents
|
|
4
|
+
# Loads named agent definitions from Markdown files and provides lookup.
|
|
5
|
+
# Scans two directories:
|
|
6
|
+
# 1. Built-in agents shipped with Anima (agents/ in the gem root)
|
|
7
|
+
# 2. User-defined agents (~/.anima/agents/)
|
|
8
|
+
# User agents override built-in ones when names collide.
|
|
9
|
+
class Registry
|
|
10
|
+
# @return [Hash{String => Definition}] loaded definitions keyed by name
|
|
11
|
+
attr_reader :agents
|
|
12
|
+
|
|
13
|
+
BUILTIN_DIR = File.expand_path("../../agents", __dir__).freeze
|
|
14
|
+
USER_DIR = File.expand_path("~/.anima/agents").freeze
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@agents = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the global registry, lazily loaded on first access.
|
|
21
|
+
#
|
|
22
|
+
# @return [Registry]
|
|
23
|
+
def self.instance
|
|
24
|
+
@instance ||= new.load_all
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reloads the global registry from disk.
|
|
28
|
+
#
|
|
29
|
+
# @return [Registry]
|
|
30
|
+
def self.reload!
|
|
31
|
+
@instance = new.load_all
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Loads definitions from both built-in and user directories.
|
|
35
|
+
# User definitions override built-in ones with the same name.
|
|
36
|
+
#
|
|
37
|
+
# @return [self]
|
|
38
|
+
def load_all
|
|
39
|
+
load_directory(BUILTIN_DIR)
|
|
40
|
+
load_directory(USER_DIR)
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Loads agent definitions from a single directory.
|
|
45
|
+
#
|
|
46
|
+
# @param dir [String] directory path to scan for .md files
|
|
47
|
+
# @return [void]
|
|
48
|
+
def load_directory(dir)
|
|
49
|
+
return unless Dir.exist?(dir)
|
|
50
|
+
|
|
51
|
+
Dir.glob(File.join(dir, "*.md")).sort.each do |path|
|
|
52
|
+
definition = Definition.from_file(path)
|
|
53
|
+
validate_tools!(definition.name, definition.tools)
|
|
54
|
+
@agents[definition.name] = definition
|
|
55
|
+
rescue InvalidDefinitionError => error
|
|
56
|
+
warn "Skipping invalid agent definition #{path}: #{error.message}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Looks up a named agent definition.
|
|
61
|
+
#
|
|
62
|
+
# @param name [String] agent name
|
|
63
|
+
# @return [Definition, nil]
|
|
64
|
+
def get(name)
|
|
65
|
+
@agents[name]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Agent names and descriptions for inclusion in tool documentation.
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash{String => String}] name => description
|
|
71
|
+
def catalog
|
|
72
|
+
@agents.transform_values(&:description)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Array<String>] registered agent names
|
|
76
|
+
def names
|
|
77
|
+
@agents.keys
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def any?
|
|
82
|
+
@agents.any?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Integer]
|
|
86
|
+
def size
|
|
87
|
+
@agents.size
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Validates that all declared tool names are recognized standard tools.
|
|
93
|
+
#
|
|
94
|
+
# @param agent_name [String] agent name for error messages
|
|
95
|
+
# @param tools [Array<String>] declared tool names
|
|
96
|
+
# @raise [InvalidDefinitionError] if any tool name is unknown
|
|
97
|
+
def validate_tools!(agent_name, tools)
|
|
98
|
+
return if tools.empty?
|
|
99
|
+
|
|
100
|
+
unknown = tools - AgentLoop::STANDARD_TOOLS_BY_NAME.keys
|
|
101
|
+
return if unknown.empty?
|
|
102
|
+
|
|
103
|
+
raise InvalidDefinitionError, "Unknown tools in '#{agent_name}': #{unknown.join(", ")}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
# Orchestrates the analytical brain — a phantom (non-persisted) LLM loop
|
|
5
|
+
# that observes a main session and performs background maintenance via tools.
|
|
6
|
+
#
|
|
7
|
+
# The analytical brain is a "subconscious" process: it operates ON the main
|
|
8
|
+
# session without the main agent knowing it exists. Tools mutate the main
|
|
9
|
+
# session directly (e.g. renaming it, activating skills), but no trace of
|
|
10
|
+
# the analytical brain's reasoning is persisted.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# AnalyticalBrain::Runner.new(session).call
|
|
14
|
+
class Runner
|
|
15
|
+
# Tools available to the analytical brain.
|
|
16
|
+
# @return [Array<Class<Tools::Base>>]
|
|
17
|
+
TOOLS = [
|
|
18
|
+
Tools::RenameSession,
|
|
19
|
+
Tools::ActivateSkill,
|
|
20
|
+
Tools::DeactivateSkill,
|
|
21
|
+
Tools::ReadWorkflow,
|
|
22
|
+
Tools::DeactivateWorkflow,
|
|
23
|
+
Tools::SetGoal,
|
|
24
|
+
Tools::UpdateGoal,
|
|
25
|
+
Tools::FinishGoal,
|
|
26
|
+
Tools::EverythingIsReady
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
30
|
+
You are a background automation that manages session metadata.
|
|
31
|
+
You MUST ONLY communicate through tool calls — NEVER output text.
|
|
32
|
+
Always finish by calling everything_is_ready.
|
|
33
|
+
|
|
34
|
+
──────────────────────────────
|
|
35
|
+
SESSION NAMING
|
|
36
|
+
──────────────────────────────
|
|
37
|
+
Call rename_session when the topic becomes clear or shifts.
|
|
38
|
+
Format: one emoji + 1-3 descriptive words.
|
|
39
|
+
|
|
40
|
+
──────────────────────────────
|
|
41
|
+
SKILL MANAGEMENT
|
|
42
|
+
──────────────────────────────
|
|
43
|
+
Call activate_skill when the conversation matches a skill's description.
|
|
44
|
+
Call deactivate_skill when the agent moves to a different domain.
|
|
45
|
+
Multiple skills can be active at once.
|
|
46
|
+
|
|
47
|
+
──────────────────────────────
|
|
48
|
+
WORKFLOW MANAGEMENT
|
|
49
|
+
──────────────────────────────
|
|
50
|
+
Call read_workflow when the user starts a multi-step task matching a workflow description.
|
|
51
|
+
Read the returned content and use judgment to create appropriate goals — not a mechanical 1:1 mapping.
|
|
52
|
+
Adapt to context: skip irrelevant steps, add extra steps for unfamiliar areas.
|
|
53
|
+
Call deactivate_workflow when the workflow completes or the user shifts focus.
|
|
54
|
+
Only one workflow can be active at a time — activating a new one replaces the previous.
|
|
55
|
+
|
|
56
|
+
──────────────────────────────
|
|
57
|
+
GOAL TRACKING
|
|
58
|
+
──────────────────────────────
|
|
59
|
+
Call set_goal to create a root goal when the user starts a multi-step task.
|
|
60
|
+
Call set_goal with parent_goal_id to add sub-goals (TODO items) under it.
|
|
61
|
+
Call update_goal to refine a goal's description as understanding evolves.
|
|
62
|
+
Call finish_goal when the main agent completes work a goal describes.
|
|
63
|
+
Finishing a root goal cascades — all active sub-goals are completed too.
|
|
64
|
+
Never duplicate an existing goal — check the active goals list first.
|
|
65
|
+
|
|
66
|
+
──────────────────────────────
|
|
67
|
+
COMPLETION
|
|
68
|
+
──────────────────────────────
|
|
69
|
+
Call everything_is_ready as your LAST tool call, every time.
|
|
70
|
+
If nothing needs changing, call it immediately as your only tool call.
|
|
71
|
+
PROMPT
|
|
72
|
+
|
|
73
|
+
# @param session [Session] the main session to observe and maintain
|
|
74
|
+
# @param client [LLM::Client, nil] injectable LLM client (defaults to fast model)
|
|
75
|
+
def initialize(session, client: nil)
|
|
76
|
+
@session = session
|
|
77
|
+
@client = client || LLM::Client.new(
|
|
78
|
+
model: Anima::Settings.fast_model,
|
|
79
|
+
max_tokens: Anima::Settings.analytical_brain_max_tokens,
|
|
80
|
+
logger: AnalyticalBrain.logger
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Runs the analytical brain loop. Builds context from the main session's
|
|
85
|
+
# recent events, calls the LLM with the analytical brain's tool set, and
|
|
86
|
+
# executes any tool calls against the main session.
|
|
87
|
+
#
|
|
88
|
+
# Events emitted during tool execution are not persisted — the phantom
|
|
89
|
+
# session_id (nil) causes the global Persister to skip them.
|
|
90
|
+
#
|
|
91
|
+
# @return [String, nil] the LLM's final text response (discarded by caller),
|
|
92
|
+
# or nil if no context is available
|
|
93
|
+
def call
|
|
94
|
+
messages = build_messages
|
|
95
|
+
sid = @session.id
|
|
96
|
+
if messages.empty?
|
|
97
|
+
log.debug("session=#{sid} — no events, skipping")
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
system = build_system_prompt
|
|
102
|
+
log.info("session=#{sid} — running (#{recent_events.size} events)")
|
|
103
|
+
log.debug("system prompt:\n#{system}")
|
|
104
|
+
log.debug("user message:\n#{messages.first[:content]}")
|
|
105
|
+
|
|
106
|
+
result = @client.chat_with_tools(
|
|
107
|
+
messages,
|
|
108
|
+
registry: build_registry,
|
|
109
|
+
session_id: nil,
|
|
110
|
+
system: system
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
log.info("session=#{sid} — done: #{result.to_s.truncate(200)}")
|
|
114
|
+
result
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Builds a condensed transcript of recent events as a single user message.
|
|
120
|
+
# The analytical brain doesn't need multi-turn conversation history — it
|
|
121
|
+
# just needs to understand "what is the agent doing RIGHT NOW?"
|
|
122
|
+
#
|
|
123
|
+
# The transcript is framed as an observation of the main session, not as
|
|
124
|
+
# a direct message to the analytical brain. Without this framing, Haiku
|
|
125
|
+
# confuses the main session's user messages with requests directed at it.
|
|
126
|
+
#
|
|
127
|
+
# @return [Array<Hash>] single-element messages array, or empty if no events
|
|
128
|
+
def build_messages
|
|
129
|
+
events = recent_events
|
|
130
|
+
return [] if events.empty?
|
|
131
|
+
|
|
132
|
+
transcript = events.filter_map { |event| format_event(event) }.join("\n")
|
|
133
|
+
content = <<~MSG.strip
|
|
134
|
+
The main session is working on this:
|
|
135
|
+
```
|
|
136
|
+
#{transcript}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Observe the conversation and take action: manage goals, activate or deactivate relevant skills, read workflows when a multi-step task matches, rename the session if needed, then call everything_is_ready.
|
|
140
|
+
MSG
|
|
141
|
+
[{role: "user", content: content}]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @return [Array<Event>] most recent events in chronological order
|
|
145
|
+
def recent_events
|
|
146
|
+
@session.events
|
|
147
|
+
.context_events
|
|
148
|
+
.reorder(id: :desc)
|
|
149
|
+
.limit(Anima::Settings.analytical_brain_event_window)
|
|
150
|
+
.to_a
|
|
151
|
+
.reverse
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Formats a single event for the analytical brain's transcript.
|
|
155
|
+
# User/agent messages get 500 chars to preserve conversation context;
|
|
156
|
+
# tool responses get 200 chars to reduce noise from verbose outputs.
|
|
157
|
+
#
|
|
158
|
+
# @param event [Event]
|
|
159
|
+
# @return [String, nil] formatted line, or nil for unhandled event types
|
|
160
|
+
def format_event(event)
|
|
161
|
+
payload = event.payload
|
|
162
|
+
summary = payload["content"].to_s.truncate(500)
|
|
163
|
+
|
|
164
|
+
case event.event_type
|
|
165
|
+
when "user_message" then "User: #{summary}"
|
|
166
|
+
when "agent_message" then "Assistant: #{summary}"
|
|
167
|
+
when "tool_call" then "Tool call: #{payload["tool_name"]}"
|
|
168
|
+
when "tool_response" then "Tool result: #{summary.truncate(200)}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Builds the system prompt with current session state, skills catalog,
|
|
173
|
+
# and currently active skills.
|
|
174
|
+
#
|
|
175
|
+
# @return [String]
|
|
176
|
+
def build_system_prompt
|
|
177
|
+
sections = [
|
|
178
|
+
SYSTEM_PROMPT,
|
|
179
|
+
session_state_section,
|
|
180
|
+
skills_catalog_section,
|
|
181
|
+
workflows_catalog_section,
|
|
182
|
+
active_goals_section
|
|
183
|
+
]
|
|
184
|
+
sections.compact.join("\n")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [String] current session name, active skills, and active workflow
|
|
188
|
+
def session_state_section
|
|
189
|
+
name = @session.name || "(unnamed)"
|
|
190
|
+
skills = @session.active_skills.join(", ").presence || "None"
|
|
191
|
+
workflow = @session.active_workflow || "None"
|
|
192
|
+
<<~SECTION
|
|
193
|
+
──────────────────────────────
|
|
194
|
+
CURRENT STATE
|
|
195
|
+
──────────────────────────────
|
|
196
|
+
Session name: #{name}
|
|
197
|
+
Active skills: #{skills}
|
|
198
|
+
Active workflow: #{workflow}
|
|
199
|
+
SECTION
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @return [String] available skills list for the analytical brain
|
|
203
|
+
def skills_catalog_section
|
|
204
|
+
catalog = Skills::Registry.instance.catalog
|
|
205
|
+
items = if catalog.empty?
|
|
206
|
+
"None"
|
|
207
|
+
else
|
|
208
|
+
catalog.map { |name, desc| "- #{name} — #{desc}" }.join("\n")
|
|
209
|
+
end
|
|
210
|
+
<<~SECTION
|
|
211
|
+
──────────────────────────────
|
|
212
|
+
AVAILABLE SKILLS
|
|
213
|
+
──────────────────────────────
|
|
214
|
+
#{items}
|
|
215
|
+
SECTION
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @return [String] available workflows list for the analytical brain
|
|
219
|
+
def workflows_catalog_section
|
|
220
|
+
catalog = Workflows::Registry.instance.catalog
|
|
221
|
+
items = if catalog.empty?
|
|
222
|
+
"None"
|
|
223
|
+
else
|
|
224
|
+
catalog.map { |name, desc| "- #{name} — #{desc}" }.join("\n")
|
|
225
|
+
end
|
|
226
|
+
<<~SECTION
|
|
227
|
+
──────────────────────────────
|
|
228
|
+
AVAILABLE WORKFLOWS
|
|
229
|
+
──────────────────────────────
|
|
230
|
+
#{items}
|
|
231
|
+
SECTION
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @return [String, nil] active goals for the brain's own context,
|
|
235
|
+
# so it knows what already exists and avoids duplicating
|
|
236
|
+
def active_goals_section
|
|
237
|
+
root_goals = @session.goals.root.includes(:sub_goals).active.order(:created_at)
|
|
238
|
+
return if root_goals.empty?
|
|
239
|
+
|
|
240
|
+
lines = root_goals.map { |goal| format_goal_for_brain(goal) }
|
|
241
|
+
<<~SECTION
|
|
242
|
+
──────────────────────────────
|
|
243
|
+
ACTIVE GOALS
|
|
244
|
+
──────────────────────────────
|
|
245
|
+
#{lines.join("\n")}
|
|
246
|
+
SECTION
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Formats a root goal and its sub-goals as a markdown checklist
|
|
250
|
+
# with IDs so the brain can reference them in finish_goal calls.
|
|
251
|
+
#
|
|
252
|
+
# @example
|
|
253
|
+
# "- Implement feature X (id: 42)\n - [x] Read code (id: 43)\n - [ ] Write tests (id: 44)"
|
|
254
|
+
#
|
|
255
|
+
# @param goal [Goal] root goal with preloaded sub_goals
|
|
256
|
+
# @return [String] goal formatted as markdown checklist for brain context
|
|
257
|
+
def format_goal_for_brain(goal)
|
|
258
|
+
parts = ["- #{goal.description} (id: #{goal.id})"]
|
|
259
|
+
goal.sub_goals.sort_by(&:created_at).each do |sub|
|
|
260
|
+
checkbox = (sub.status == "completed") ? "[x]" : "[ ]"
|
|
261
|
+
parts << " - #{checkbox} #{sub.description} (id: #{sub.id})"
|
|
262
|
+
end
|
|
263
|
+
parts.join("\n")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# @return [Logger] dev-only analytical brain logger
|
|
267
|
+
def log = AnalyticalBrain.logger
|
|
268
|
+
|
|
269
|
+
# @return [Tools::Registry] registry with analytical brain tools
|
|
270
|
+
def build_registry
|
|
271
|
+
registry = ::Tools::Registry.new(context: {main_session: @session})
|
|
272
|
+
TOOLS.each { |tool| registry.register(tool) }
|
|
273
|
+
registry
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Activates a domain knowledge skill on the main session.
|
|
6
|
+
# The skill's content is injected into the main agent's system prompt,
|
|
7
|
+
# making the knowledge available for the current and future responses.
|
|
8
|
+
class ActivateSkill < ::Tools::Base
|
|
9
|
+
def self.tool_name = "activate_skill"
|
|
10
|
+
|
|
11
|
+
def self.description = "Activate a domain knowledge skill on the main session. " \
|
|
12
|
+
"The skill's content will be injected into the agent's system prompt."
|
|
13
|
+
|
|
14
|
+
def self.input_schema
|
|
15
|
+
{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
name: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Name of the skill to activate (from the available skills list)"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
required: %w[name]
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param main_session [Session] the session to activate the skill on
|
|
28
|
+
def initialize(main_session:, **)
|
|
29
|
+
@main_session = main_session
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param input [Hash<String, Object>] with "name" key
|
|
33
|
+
# @return [String] confirmation message with skill description
|
|
34
|
+
# @return [Hash] with :error key on validation failure
|
|
35
|
+
def execute(input)
|
|
36
|
+
skill_name = input["name"].to_s.strip
|
|
37
|
+
return {error: "Skill name cannot be blank"} if skill_name.empty?
|
|
38
|
+
|
|
39
|
+
skill = @main_session.activate_skill(skill_name)
|
|
40
|
+
format_confirmation(skill)
|
|
41
|
+
rescue Skills::InvalidDefinitionError => error
|
|
42
|
+
{error: error.message}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def format_confirmation(skill)
|
|
48
|
+
"Activated skill: #{skill.name} (#{skill.description})"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Deactivates a domain knowledge skill on the main session.
|
|
6
|
+
# The skill's content is removed from the main agent's system prompt.
|
|
7
|
+
class DeactivateSkill < ::Tools::Base
|
|
8
|
+
def self.tool_name = "deactivate_skill"
|
|
9
|
+
|
|
10
|
+
def self.description = "Deactivate a skill that is no longer relevant. " \
|
|
11
|
+
"The skill's content will be removed from the agent's system prompt."
|
|
12
|
+
|
|
13
|
+
def self.input_schema
|
|
14
|
+
{
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
name: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Name of the skill to deactivate (from the currently active skills list)"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
required: %w[name]
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param main_session [Session] the session to deactivate the skill on
|
|
27
|
+
def initialize(main_session:, **)
|
|
28
|
+
@main_session = main_session
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param input [Hash<String, Object>] with "name" key
|
|
32
|
+
# @return [String] confirmation message
|
|
33
|
+
# @return [Hash] with :error key on validation failure
|
|
34
|
+
def execute(input)
|
|
35
|
+
skill_name = input["name"].to_s.strip
|
|
36
|
+
return {error: "Skill name cannot be blank"} if skill_name.empty?
|
|
37
|
+
|
|
38
|
+
@main_session.deactivate_skill(skill_name)
|
|
39
|
+
"Deactivated skill: #{skill_name}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Deactivates the current workflow on the main session.
|
|
6
|
+
# The workflow's content is removed from the main agent's system prompt.
|
|
7
|
+
class DeactivateWorkflow < ::Tools::Base
|
|
8
|
+
def self.tool_name = "deactivate_workflow"
|
|
9
|
+
|
|
10
|
+
def self.description = "Deactivate the current workflow when it is complete or no longer relevant."
|
|
11
|
+
|
|
12
|
+
def self.input_schema
|
|
13
|
+
{
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {},
|
|
16
|
+
required: []
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param main_session [Session] the session to deactivate the workflow on
|
|
21
|
+
def initialize(main_session:, **)
|
|
22
|
+
@main_session = main_session
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param input [Hash<String, Object>] (no parameters needed)
|
|
26
|
+
# @return [String] confirmation message
|
|
27
|
+
def execute(_input)
|
|
28
|
+
previous = @main_session.active_workflow
|
|
29
|
+
@main_session.deactivate_workflow
|
|
30
|
+
previous ? "Deactivated workflow: #{previous}" : "No workflow was active"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticalBrain
|
|
4
|
+
module Tools
|
|
5
|
+
# Terminal tool that signals the analytical brain has completed its work.
|
|
6
|
+
# Call this when no changes are needed — the current session state is
|
|
7
|
+
# already good.
|
|
8
|
+
#
|
|
9
|
+
# After this tool returns, the LLM responds with text (not another
|
|
10
|
+
# tool call), naturally terminating the chat_with_tools loop.
|
|
11
|
+
class EverythingIsReady < ::Tools::Base
|
|
12
|
+
def self.tool_name = "everything_is_ready"
|
|
13
|
+
|
|
14
|
+
def self.description = "Signal that no changes are needed. " \
|
|
15
|
+
"Call this when the session name and active skills are already appropriate."
|
|
16
|
+
|
|
17
|
+
def self.input_schema
|
|
18
|
+
{type: "object", properties: {}, required: []}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param _input [Hash] ignored — this tool takes no input
|
|
22
|
+
# @return [String] confirmation message
|
|
23
|
+
def execute(_input)
|
|
24
|
+
"Acknowledged. No changes needed."
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|