aia 1.1.1 → 2.0.0.0.pre.beta2
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/.envrc +9 -1
- data/.loki +11 -0
- data/.reek.yml +160 -0
- data/.rubocop.yml +116 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +269 -55
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +267 -239
- data/Rakefile +5 -5
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/architecture_review.md +314 -0
- data/bin/aia +16 -0
- data/config/aia.yml +13 -0
- data/docs/AGENTS.md +40 -0
- data/docs/advanced-prompting.md +67 -3
- data/docs/cli-reference.md +312 -56
- data/docs/configuration.md +130 -19
- data/docs/contributing.md +56 -2
- data/docs/directives-reference.md +593 -78
- data/docs/faq.md +85 -3
- data/docs/guides/available-models.md +1 -1
- data/docs/guides/basic-usage.md +6 -6
- data/docs/guides/chat.md +40 -16
- data/docs/guides/crew.md +239 -0
- data/docs/guides/executable-prompts.md +1 -1
- data/docs/guides/index.md +1 -0
- data/docs/guides/models.md +15 -0
- data/docs/index.md +29 -2
- data/docs/installation.md +44 -17
- data/docs/mcp-integration.md +40 -0
- data/docs/prompt_management.md +85 -86
- data/docs/security.md +47 -0
- data/docs/special_projects_guide.md +386 -0
- data/docs/tools-and-mcp-examples.md +23 -0
- data/docs/workflows-and-pipelines.md +84 -7
- data/examples/.gitignore +1 -0
- data/examples/00_setup_aia.sh +27 -44
- data/examples/11_multi_model.sh +4 -14
- data/examples/12_token_usage.sh +3 -12
- data/examples/18_tools.sh +10 -2
- data/examples/22_chat_mode.sh +0 -10
- data/examples/23_verify.sh +139 -0
- data/examples/24_decompose.sh +139 -0
- data/examples/25_spawn.sh +139 -0
- data/examples/26_debate.sh +97 -0
- data/examples/27_mention_routing.sh +157 -0
- data/examples/28_model_switching.sh +106 -0
- data/examples/29_agent_harness.sh +177 -0
- data/examples/README.md +65 -0
- data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
- data/examples/aia_config.yml +1 -1
- data/examples/aia_config_orchestrator.yml +45 -0
- data/examples/common.sh +18 -6
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/roles/orchestrator.md +21 -0
- data/examples/requirements/sinatra_taskflow_app.md +139 -0
- data/examples/rules/01_classify_ruby.rb +16 -0
- data/examples/rules/02_prefer_claude_for_code.rb +19 -0
- data/examples/rules/03_gate_prompt_length.rb +19 -0
- data/examples/rules/04_tool_selection.rb +41 -0
- data/examples/rules/README.md +30 -0
- data/examples/run_all.sh +48 -15
- data/examples/tools/word_count_tool.rb +1 -1
- data/lib/AGENTS.md +57 -0
- data/lib/aia/chat_loop.rb +263 -167
- data/lib/aia/config/cli_parser.rb +217 -145
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +52 -51
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +171 -216
- data/lib/aia/config.rb +111 -145
- data/lib/aia/content_extractor.rb +155 -0
- data/lib/aia/cost_calculator.rb +39 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +174 -0
- data/lib/aia/delegate_handler.rb +116 -0
- data/lib/aia/directive.rb +43 -26
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +214 -60
- data/lib/aia/directives/context_directives.rb +67 -52
- data/lib/aia/directives/execution_directives.rb +141 -4
- data/lib/aia/directives/model_directives.rb +163 -141
- data/lib/aia/directives/trakflow_directives.rb +62 -0
- data/lib/aia/directives/utility_directives.rb +227 -30
- data/lib/aia/directives/web_and_file_directives.rb +126 -77
- data/lib/aia/errors.rb +15 -0
- data/lib/aia/fact_asserter.rb +27 -0
- data/lib/aia/fzf.rb +9 -31
- data/lib/aia/handler_context.rb +17 -0
- data/lib/aia/handler_protocol.rb +19 -0
- data/lib/aia/history_transfer.rb +55 -0
- data/lib/aia/input_collector.rb +3 -3
- data/lib/aia/layered_orchestrator.rb +471 -0
- data/lib/aia/logger.rb +45 -25
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +315 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +60 -0
- data/lib/aia/mention_router.rb +217 -0
- data/lib/aia/model_alias_registry.rb +97 -0
- data/lib/aia/model_switch_handler.rb +100 -0
- data/lib/aia/network_builder.rb +160 -0
- data/lib/aia/network_memory_manager.rb +55 -0
- data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
- data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
- data/lib/aia/pipeline_orchestrator.rb +272 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +211 -0
- data/lib/aia/prompt_decomposer.rb +159 -0
- data/lib/aia/prompt_handler.rb +56 -82
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +338 -0
- data/lib/aia/robot_namer.rb +110 -0
- data/lib/aia/session.rb +87 -17
- data/lib/aia/session_tracker.rb +207 -0
- data/lib/aia/similarity_scorer.rb +41 -0
- data/lib/aia/skill_utils.rb +105 -1
- data/lib/aia/spawn_handler.rb +129 -0
- data/lib/aia/spawn_spec_parser.rb +65 -0
- data/lib/aia/special_mode_handler.rb +322 -0
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +151 -0
- data/lib/aia/streaming_runner.rb +172 -0
- data/lib/aia/system_prompt_assembler.rb +92 -0
- data/lib/aia/task_coordinator.rb +207 -0
- data/lib/aia/task_decomposer.rb +57 -0
- data/lib/aia/task_executor.rb +51 -0
- data/lib/aia/tfidf_math.rb +27 -0
- data/lib/aia/timing.rb +15 -0
- data/lib/aia/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +83 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +146 -0
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +216 -0
- data/lib/aia/tool_utility.rb +30 -0
- data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
- data/lib/aia/tools/recruit_robot_tool.rb +60 -0
- data/lib/aia/tools/reskill_robot_tool.rb +44 -0
- data/lib/aia/tools/task_board_tool.rb +115 -0
- data/lib/aia/trakflow_bridge.rb +175 -0
- data/lib/aia/turn_state.rb +95 -0
- data/lib/aia/ui_presenter.rb +182 -206
- data/lib/aia/utility.rb +136 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
- data/lib/aia/verification_network.rb +57 -0
- data/lib/aia.rb +124 -63
- data/mkdocs.yml +1 -0
- metadata +187 -58
- data/justfile +0 -215
- data/lib/aia/adapter/chat_execution.rb +0 -242
- data/lib/aia/adapter/error_handler.rb +0 -68
- data/lib/aia/adapter/gem_activator.rb +0 -57
- data/lib/aia/adapter/mcp_connector.rb +0 -274
- data/lib/aia/adapter/modality_handlers.rb +0 -167
- data/lib/aia/adapter/model_registry.rb +0 -81
- data/lib/aia/adapter/multi_model_chat.rb +0 -218
- data/lib/aia/adapter/provider_configurator.rb +0 -59
- data/lib/aia/adapter/tool_filter.rb +0 -85
- data/lib/aia/adapter/tool_loader.rb +0 -90
- data/lib/aia/chat_processor_service.rb +0 -178
- data/lib/aia/prompt_pipeline.rb +0 -183
- data/lib/aia/ruby_llm_adapter.rb +0 -95
- data/lib/extensions/openstruct_merge.rb +0 -48
- data/lib/extensions/ruby_llm/.irbrc +0 -56
- data/lib/extensions/ruby_llm/modalities.rb +0 -36
- data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
- data/lib/refinements/string.rb +0 -16
- data/main.just +0 -76
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/tool_loader.rb
|
|
4
|
+
#
|
|
5
|
+
# Tool loading, caching, filtering, and discovery.
|
|
6
|
+
# Extracted from RobotFactory to isolate the tool concern.
|
|
7
|
+
#
|
|
8
|
+
# ToolLoader is a class with instance-level cache state so that two instances
|
|
9
|
+
# have independent caches. Class-level convenience methods delegate to a
|
|
10
|
+
# resettable default instance (reset via ToolLoader.reset_instance! or AIA.reset!).
|
|
11
|
+
|
|
12
|
+
module AIA
|
|
13
|
+
class ToolLoader
|
|
14
|
+
class << self
|
|
15
|
+
# Returns the shared default instance, creating it on first call.
|
|
16
|
+
def instance
|
|
17
|
+
@instance ||= new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Discard the default instance. The next call to .instance creates a fresh one.
|
|
21
|
+
# Called from AIA.reset! for test isolation.
|
|
22
|
+
def reset_instance!
|
|
23
|
+
@instance = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Class-level convenience wrappers — delegate to the default instance.
|
|
27
|
+
def cached_tools = instance.cached_tools
|
|
28
|
+
def clear_cache! = instance.clear_cache!
|
|
29
|
+
def load_tools(config) = instance.load_tools(config)
|
|
30
|
+
def filtered_tools(config) = instance.filtered_tools(config)
|
|
31
|
+
def discover_tools = instance.discover_tools
|
|
32
|
+
def eager_load_gem_tools = instance.eager_load_gem_tools
|
|
33
|
+
|
|
34
|
+
def activate_unbundled_gem(name)
|
|
35
|
+
instance.activate_unbundled_gem(name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize
|
|
40
|
+
@tool_cache = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clear the cached tool discovery results.
|
|
44
|
+
# Call when tool paths change via /config directive.
|
|
45
|
+
def clear_cache!
|
|
46
|
+
@tool_cache = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Return the current tool cache (nil if not yet populated).
|
|
50
|
+
def cached_tools
|
|
51
|
+
@tool_cache
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Load tools from require_libs and tool paths, then cache the result.
|
|
55
|
+
# Subsequent calls to RobotFactory.build skip this entirely if cache exists.
|
|
56
|
+
# :reek:TooManyStatements -- sequential phases: require_libs with activation retry, tool files, eager loads, discovery cache
|
|
57
|
+
def load_tools(config)
|
|
58
|
+
Array(config.require_libs).each do |lib|
|
|
59
|
+
require lib
|
|
60
|
+
rescue LoadError
|
|
61
|
+
# Not yet active — activate it (and its deps) via RubyGems, then retry
|
|
62
|
+
if activate_unbundled_gem(lib)
|
|
63
|
+
require lib rescue warn("Warning: Failed to require '#{lib}' after activation")
|
|
64
|
+
else
|
|
65
|
+
$stderr.puts "Warning: Failed to require '#{lib}': gem not found"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Load tool files from paths
|
|
70
|
+
Array(config.tools&.paths).each do |path|
|
|
71
|
+
expanded = File.expand_path(path)
|
|
72
|
+
if File.exist?(expanded)
|
|
73
|
+
require expanded
|
|
74
|
+
else
|
|
75
|
+
$stderr.puts "Warning: Tool file not found: #{path}"
|
|
76
|
+
end
|
|
77
|
+
rescue LoadError, StandardError => e
|
|
78
|
+
$stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Eagerly load tools from gems that use zeitwerk lazy loading
|
|
82
|
+
# (e.g., shared_tools provides .load_all_tools for this purpose)
|
|
83
|
+
eager_load_gem_tools
|
|
84
|
+
|
|
85
|
+
# Re-register PM directives so any AIA::Directive subclasses defined
|
|
86
|
+
# in just-loaded tool files become available in ERB prompt rendering.
|
|
87
|
+
PM::Directive.register_all if defined?(PM::Directive)
|
|
88
|
+
|
|
89
|
+
# Discover loaded tools via ObjectSpace and cache the result
|
|
90
|
+
tools = discover_tools
|
|
91
|
+
@tool_cache = tools
|
|
92
|
+
config.loaded_tools = tools
|
|
93
|
+
config.tool_names = tools.map { |t| tool_name(t) }.join(', ')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A tool's display name: instances expose #name; bare classes fall back to the class name.
|
|
97
|
+
def tool_name(tool)
|
|
98
|
+
tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Filter tools based on allowed/rejected lists and KBS decisions.
|
|
102
|
+
# :reek:TooManyStatements -- allowed filter, rejected filter, then a name-dedup pass over the tool list
|
|
103
|
+
# :reek:DuplicateMethodCall -- tool_name(t) is applied per element inside three separate filter blocks; no shared scope to hoist into
|
|
104
|
+
def filtered_tools(config)
|
|
105
|
+
tools = config.loaded_tools || []
|
|
106
|
+
allowed = config.tools&.allowed
|
|
107
|
+
rejected = config.tools&.rejected
|
|
108
|
+
|
|
109
|
+
if allowed && !allowed.empty?
|
|
110
|
+
allowed_list = Array(allowed).map(&:strip).map(&:downcase)
|
|
111
|
+
tools = tools.select do |t|
|
|
112
|
+
name = tool_name(t).downcase
|
|
113
|
+
# name is a String; Array#intersect? would raise TypeError
|
|
114
|
+
allowed_list.any? { |a| name.include?(a) }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if rejected && !rejected.empty?
|
|
119
|
+
rejected_list = Array(rejected).map(&:strip).map(&:downcase)
|
|
120
|
+
tools = tools.reject do |t|
|
|
121
|
+
name = tool_name(t).downcase
|
|
122
|
+
# name is a String; Array#intersect? would raise TypeError
|
|
123
|
+
rejected_list.any? { |r| name.include?(r) }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
seen = {}
|
|
128
|
+
tools.select do |t|
|
|
129
|
+
name = tool_name(t)
|
|
130
|
+
if seen[name]
|
|
131
|
+
false
|
|
132
|
+
else
|
|
133
|
+
seen[name] = true
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Discover RubyLLM::Tool subclasses from ObjectSpace.
|
|
140
|
+
# Skips tools that report themselves as unavailable via #available?.
|
|
141
|
+
def discover_tools
|
|
142
|
+
ObjectSpace.each_object(Class).select do |klass|
|
|
143
|
+
next false unless defined?(RubyLLM::Tool) && klass < RubyLLM::Tool
|
|
144
|
+
begin
|
|
145
|
+
instance = klass.new
|
|
146
|
+
if instance.respond_to?(:available?) && !instance.available?
|
|
147
|
+
tool_name = instance.respond_to?(:name) ? instance.name : klass.name
|
|
148
|
+
$stderr.puts "Info: Tool '#{tool_name}' is not available, skipping"
|
|
149
|
+
next false
|
|
150
|
+
end
|
|
151
|
+
true
|
|
152
|
+
rescue LoadError, StandardError
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Activate a gem that isn't yet active by asking RubyGems to activate it.
|
|
159
|
+
# This adds its lib paths to $LOAD_PATH AND activates its dependencies,
|
|
160
|
+
# unlike a manual $LOAD_PATH manipulation which would leave deps unresolved.
|
|
161
|
+
#
|
|
162
|
+
# Under bundler/setup (dev mode), `gem name` raises Gem::LoadError for any
|
|
163
|
+
# gem not in the bundle. The fallback searches known gem install paths directly
|
|
164
|
+
# and adds the lib dir to $LOAD_PATH so Zeitwerk-based gems still load.
|
|
165
|
+
def activate_unbundled_gem(name)
|
|
166
|
+
gem name
|
|
167
|
+
true
|
|
168
|
+
rescue Gem::LoadError
|
|
169
|
+
lib_dir = [Gem.default_dir, Gem.user_dir].compact.flat_map do |d|
|
|
170
|
+
Dir.glob("#{d}/gems/#{name}-*/lib")
|
|
171
|
+
end.max
|
|
172
|
+
return false unless lib_dir
|
|
173
|
+
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
|
174
|
+
true
|
|
175
|
+
rescue StandardError
|
|
176
|
+
false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Eagerly load tool classes from any required gem that uses lazy loading
|
|
180
|
+
# (e.g. Zeitwerk). Calls .load_all_tools on every module that responds to it,
|
|
181
|
+
# so any gem following that convention works — not just SharedTools.
|
|
182
|
+
#
|
|
183
|
+
# Gem::LoadError (a LoadError, not a StandardError) can fire when a tool's
|
|
184
|
+
# dependency has a version conflict with an already-activated gem. The inner
|
|
185
|
+
# rescue handles that per-module; the fallback loads constants one-at-a-time
|
|
186
|
+
# so tools without conflicting deps still become available.
|
|
187
|
+
def eager_load_gem_tools
|
|
188
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
189
|
+
if mod.respond_to?(:load_all_tools)
|
|
190
|
+
begin
|
|
191
|
+
mod.load_all_tools
|
|
192
|
+
rescue LoadError, StandardError
|
|
193
|
+
eager_load_namespace_fallback(mod)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
rescue LoadError, StandardError
|
|
197
|
+
next
|
|
198
|
+
end
|
|
199
|
+
rescue LoadError, StandardError => e
|
|
200
|
+
$stderr.puts "Warning: Failed to eager-load gem tools: #{e.message}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Fallback when bulk load_all_tools fails: walk the module's namespace
|
|
204
|
+
# recursively (up to 3 levels) and trigger each constant's autoload
|
|
205
|
+
# individually, skipping any that raise a load error.
|
|
206
|
+
def eager_load_namespace_fallback(mod, depth = 0)
|
|
207
|
+
return if depth > 3
|
|
208
|
+
mod.constants.each do |const_name|
|
|
209
|
+
child = mod.const_get(const_name)
|
|
210
|
+
eager_load_namespace_fallback(child, depth + 1) if child.is_a?(Module)
|
|
211
|
+
rescue LoadError, StandardError
|
|
212
|
+
next
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/tool_utility.rb
|
|
4
|
+
#
|
|
5
|
+
# Pure tool state-query methods mixed into AIA::Utility via `class << self include`.
|
|
6
|
+
# No display or formatting concerns — those live in Utility directly.
|
|
7
|
+
|
|
8
|
+
module AIA
|
|
9
|
+
module ToolUtility
|
|
10
|
+
def tools?
|
|
11
|
+
return true if AIA.config&.tool_names && !AIA.config.tool_names.empty?
|
|
12
|
+
total_tool_count.positive?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def total_tool_count
|
|
16
|
+
local = Array(AIA.config&.loaded_tools).size
|
|
17
|
+
return local if AIA.config&.flags&.no_mcp
|
|
18
|
+
mcp = defined?(RubyLLM::MCP) ? RubyLLM::MCP.clients.sum { |_, c| c.tools.count } : 0
|
|
19
|
+
local + mcp
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def user_tools?
|
|
23
|
+
AIA.config&.tools&.paths&.any?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def supports_tools?
|
|
27
|
+
AIA.client&.model&.supports_functions? || false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/tools/delegate_to_foreman_tool.rb
|
|
4
|
+
#
|
|
5
|
+
# Lightweight tool given to Tobor (and other primary robots) that
|
|
6
|
+
# delegates task board operations to Foreman, a specialist robot
|
|
7
|
+
# with full TaskBoardTool access. Foreman is built on first use
|
|
8
|
+
# and cached for the session.
|
|
9
|
+
|
|
10
|
+
module AIA
|
|
11
|
+
class DelegateToForemanTool < RubyLLM::Tool
|
|
12
|
+
description "Delegate a task management request to Foreman, the task board manager. " \
|
|
13
|
+
"Use this when you need to create, assign, track, or manage tasks. " \
|
|
14
|
+
"Pass a natural language description of what you need done on the task board."
|
|
15
|
+
|
|
16
|
+
param :request, type: :string, required: true,
|
|
17
|
+
desc: "Natural language description of the task board operation " \
|
|
18
|
+
"(e.g., 'create a research task about recessions assigned to Tobor with labels research, economics')"
|
|
19
|
+
|
|
20
|
+
def available?
|
|
21
|
+
AIA.task_coordinator&.available? || false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute(request:, **_params)
|
|
25
|
+
foreman = self.class.foreman
|
|
26
|
+
result = foreman.run(request)
|
|
27
|
+
result.respond_to?(:reply) ? result.reply : result.to_s
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
"Foreman error: #{e.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def foreman
|
|
34
|
+
@foreman ||= build_foreman
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset!
|
|
38
|
+
@foreman = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_foreman
|
|
44
|
+
model_spec = AIA.config.models.first
|
|
45
|
+
run_config = RobotFactory.build_run_config(AIA.config)
|
|
46
|
+
|
|
47
|
+
RobotFactory.build_robot(
|
|
48
|
+
model_spec,
|
|
49
|
+
name: 'Foreman',
|
|
50
|
+
system_prompt: foreman_system_prompt,
|
|
51
|
+
local_tools: [TaskBoardTool],
|
|
52
|
+
config: run_config
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def foreman_system_prompt
|
|
57
|
+
<<~PROMPT
|
|
58
|
+
You are Foreman, the task board manager. You manage a shared TrakFlow
|
|
59
|
+
task board where robots can create, claim, and complete tasks.
|
|
60
|
+
|
|
61
|
+
When given a request, use the task_board tool to execute it. Be precise
|
|
62
|
+
and efficient — execute the operation and report the result concisely.
|
|
63
|
+
|
|
64
|
+
You can: create tasks, plan multi-step work, check ready tasks, claim
|
|
65
|
+
tasks, mark tasks complete, block tasks, and report board status.
|
|
66
|
+
PROMPT
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/tools/recruit_robot_tool.rb
|
|
4
|
+
#
|
|
5
|
+
# RubyLLM::Tool that lets the crew chief grow its own crew. The chief can
|
|
6
|
+
# name a new robot, pick its provider/model (or inherit the chief's), and
|
|
7
|
+
# write its system prompt — all through standard tool-use protocol. The
|
|
8
|
+
# newcomer joins the running crew and answers to @name immediately.
|
|
9
|
+
|
|
10
|
+
module AIA
|
|
11
|
+
class RecruitRobotTool < RubyLLM::Tool
|
|
12
|
+
description "Recruit a new robot into the crew. Give it a name, an " \
|
|
13
|
+
"optional provider/model (omit to inherit yours), optional " \
|
|
14
|
+
"skills that define its role, and/or a system prompt. The " \
|
|
15
|
+
"newcomer joins the crew right away and can be addressed with @name."
|
|
16
|
+
|
|
17
|
+
param :name, type: :string, required: true,
|
|
18
|
+
desc: "Unique name for the new robot, e.g. 'researcher'. Reserved: 'crew'."
|
|
19
|
+
param :model, type: :string,
|
|
20
|
+
desc: "Optional provider/model, e.g. 'ollama/qwen3.6:latest' or 'gpt-4o'. " \
|
|
21
|
+
"Omit (or pass '-') to inherit the chief's model and provider."
|
|
22
|
+
param :skills, type: :string,
|
|
23
|
+
desc: "Optional comma-separated skill ids to assign as the robot's role " \
|
|
24
|
+
"(e.g. 'security-review,ruby-style'). Each skill's content becomes " \
|
|
25
|
+
"part of its system prompt. Use this to give crew members distinct jobs."
|
|
26
|
+
param :system_prompt, type: :string,
|
|
27
|
+
desc: "Optional extra system prompt, appended after any skills — its role, " \
|
|
28
|
+
"persona, and instructions."
|
|
29
|
+
|
|
30
|
+
def execute(name:, model: nil, skills: nil, system_prompt: nil)
|
|
31
|
+
robot = AIA::Crew.recruit(build_spec(name, model, skills, system_prompt))
|
|
32
|
+
"Recruited '#{robot.name}' into the crew on #{robot.model || 'the chief\'s model'}. " \
|
|
33
|
+
"Address it with @#{robot.name}."
|
|
34
|
+
rescue AIA::CrewError => e
|
|
35
|
+
"Could not recruit '#{name}': #{e.message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Translate the tool's flat params into a Crew spec, splitting a
|
|
41
|
+
# "provider/model" string into its parts (nil model => inherit the chief's)
|
|
42
|
+
# and a comma-separated skills string into a list.
|
|
43
|
+
def build_spec(name, model, skills, system_prompt)
|
|
44
|
+
parsed_model, provider = SpawnSpecParser.parse_model(model)
|
|
45
|
+
prompt = system_prompt.to_s.strip
|
|
46
|
+
{
|
|
47
|
+
name: name,
|
|
48
|
+
model: parsed_model,
|
|
49
|
+
provider: provider,
|
|
50
|
+
skills: split_skills(skills),
|
|
51
|
+
system_prompt: prompt.empty? ? nil : prompt
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# "security, ruby-style" => ["security", "ruby-style"]
|
|
56
|
+
def split_skills(skills)
|
|
57
|
+
skills.to_s.split(',').map(&:strip).reject(&:empty?)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/tools/reskill_robot_tool.rb
|
|
4
|
+
#
|
|
5
|
+
# RubyLLM::Tool that lets the crew chief reset one of its crew members to a
|
|
6
|
+
# clean slate and give it a different role. The member keeps its name and model
|
|
7
|
+
# but gets a fresh conversation and a new skill/system prompt — useful when a
|
|
8
|
+
# recruit drifted off task or needs to be repurposed for the next phase.
|
|
9
|
+
|
|
10
|
+
module AIA
|
|
11
|
+
class ReskillRobotTool < RubyLLM::Tool
|
|
12
|
+
description "Reset a crew member to a clean slate and re-skill it. The robot " \
|
|
13
|
+
"keeps its @name and model but gets a fresh conversation and a new " \
|
|
14
|
+
"role from the given skills and/or system prompt. Use this to " \
|
|
15
|
+
"repurpose a member or recover one that went off task."
|
|
16
|
+
|
|
17
|
+
param :name, type: :string, required: true,
|
|
18
|
+
desc: "Name of the existing crew member to reset (cannot be the chief)."
|
|
19
|
+
param :skills, type: :string,
|
|
20
|
+
desc: "Optional comma-separated skill ids to assign as the new role " \
|
|
21
|
+
"(e.g. 'testing,performance')."
|
|
22
|
+
param :system_prompt, type: :string,
|
|
23
|
+
desc: "Optional extra system prompt, appended after any skills."
|
|
24
|
+
|
|
25
|
+
def execute(name:, skills: nil, system_prompt: nil)
|
|
26
|
+
prompt = system_prompt.to_s.strip
|
|
27
|
+
robot = AIA::Crew.reskill(
|
|
28
|
+
name,
|
|
29
|
+
skills: split_skills(skills),
|
|
30
|
+
system_prompt: prompt.empty? ? nil : prompt
|
|
31
|
+
)
|
|
32
|
+
"Reset '#{robot.name}' to a clean slate with its new role. Address it with @#{robot.name}."
|
|
33
|
+
rescue AIA::CrewError => e
|
|
34
|
+
"Could not reskill '#{name}': #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# "testing, performance" => ["testing", "performance"]
|
|
40
|
+
def split_skills(skills)
|
|
41
|
+
skills.to_s.split(',').map(&:strip).reject(&:empty?)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/tools/task_board_tool.rb
|
|
4
|
+
#
|
|
5
|
+
# RubyLLM::Tool that lets robots manage a shared TrakFlow task board.
|
|
6
|
+
# Robots can create tasks for each other, check what's ready, claim
|
|
7
|
+
# work, and report completion — all through standard tool-use protocol.
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
|
|
11
|
+
module AIA
|
|
12
|
+
# :reek:DataClump -- each execute_<action> dispatch target receives the same LLM tool-call context
|
|
13
|
+
class TaskBoardTool < RubyLLM::Tool
|
|
14
|
+
description "Manage a shared task board. Create tasks for other robots, " \
|
|
15
|
+
"check what's ready, claim tasks, and report completion."
|
|
16
|
+
|
|
17
|
+
param :action, type: :string, required: true,
|
|
18
|
+
desc: "One of: create, plan, ready, claim, complete, block, status"
|
|
19
|
+
param :title, type: :string,
|
|
20
|
+
desc: "Task title (for create/plan actions)"
|
|
21
|
+
param :assignee, type: :string,
|
|
22
|
+
desc: "Robot name to assign to (for create/claim/ready)"
|
|
23
|
+
param :labels, type: :string,
|
|
24
|
+
desc: "Comma-separated labels (for create)"
|
|
25
|
+
param :blocked_by, type: :string,
|
|
26
|
+
desc: "Comma-separated blocker task IDs (for create)"
|
|
27
|
+
param :steps, type: :string,
|
|
28
|
+
desc: "JSON array of step objects [{title:, assignee:}] (for plan)"
|
|
29
|
+
param :task_id, type: :string,
|
|
30
|
+
desc: "Task ID (for claim/complete/block)"
|
|
31
|
+
param :result, type: :string,
|
|
32
|
+
desc: "Result summary (for complete) or reason (for block)"
|
|
33
|
+
|
|
34
|
+
# Never auto-discovered onto primary robots. Only Foreman
|
|
35
|
+
# (built by DelegateToForemanTool) receives this tool explicitly.
|
|
36
|
+
def available?
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execute(action:, **params)
|
|
41
|
+
coordinator = AIA.task_coordinator
|
|
42
|
+
return "Task board unavailable (TrakFlow not initialized)" unless coordinator&.available?
|
|
43
|
+
|
|
44
|
+
robot_name = params[:_robot_name] || "unknown"
|
|
45
|
+
|
|
46
|
+
case action
|
|
47
|
+
when "create" then execute_create(coordinator, robot_name, params)
|
|
48
|
+
when "plan" then execute_plan(coordinator, robot_name, params)
|
|
49
|
+
when "ready" then execute_ready(coordinator, params)
|
|
50
|
+
when "claim" then execute_claim(coordinator, robot_name, params)
|
|
51
|
+
when "complete" then execute_complete(coordinator, robot_name, params)
|
|
52
|
+
when "block" then execute_block(coordinator, robot_name, params)
|
|
53
|
+
when "status" then coordinator.status_summary || "No tasks"
|
|
54
|
+
else
|
|
55
|
+
"Unknown action: #{action}. Use: create, plan, ready, claim, complete, block, status"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def execute_create(coordinator, robot_name, params)
|
|
62
|
+
task = coordinator.create_task(
|
|
63
|
+
params[:title],
|
|
64
|
+
assignee: params[:assignee],
|
|
65
|
+
labels: parse_csv(params[:labels]),
|
|
66
|
+
blocked_by: parse_csv(params[:blocked_by]),
|
|
67
|
+
creator: robot_name
|
|
68
|
+
)
|
|
69
|
+
task ? "Created [#{task.id}] '#{task.title}'" : "Failed to create task"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def execute_plan(coordinator, robot_name, params)
|
|
73
|
+
steps = JSON.parse(params[:steps] || "[]", symbolize_names: true)
|
|
74
|
+
result = coordinator.create_plan(
|
|
75
|
+
params[:title], steps: steps, creator: robot_name
|
|
76
|
+
)
|
|
77
|
+
result ? "Plan created with #{result[:steps].size} steps" : "Failed to create plan"
|
|
78
|
+
rescue JSON::ParserError => e
|
|
79
|
+
"Invalid steps JSON: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def execute_ready(coordinator, params)
|
|
83
|
+
tasks = coordinator.ready_tasks(robot_name: params[:assignee])
|
|
84
|
+
return "No ready tasks" if tasks.empty?
|
|
85
|
+
tasks.map { |t| "[#{t.id}] #{t.title} (#{t.assignee || 'unassigned'})" }.join("\n")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def execute_claim(coordinator, robot_name, params)
|
|
89
|
+
coordinator.claim_task(params[:task_id], robot_name)
|
|
90
|
+
"Claimed task #{params[:task_id]}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def execute_complete(coordinator, robot_name, params)
|
|
94
|
+
coordinator.complete_task(
|
|
95
|
+
params[:task_id],
|
|
96
|
+
result: params[:result] || "Done",
|
|
97
|
+
robot_name: robot_name
|
|
98
|
+
)
|
|
99
|
+
"Completed task #{params[:task_id]}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def execute_block(coordinator, robot_name, params)
|
|
103
|
+
coordinator.block_task(
|
|
104
|
+
params[:task_id],
|
|
105
|
+
reason: params[:result] || "Blocked",
|
|
106
|
+
robot_name: robot_name
|
|
107
|
+
)
|
|
108
|
+
"Blocked task #{params[:task_id]}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_csv(value)
|
|
112
|
+
(value || "").split(",").map(&:strip).reject(&:empty?)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|