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.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +9 -1
  3. data/.loki +11 -0
  4. data/.reek.yml +160 -0
  5. data/.rubocop.yml +116 -0
  6. data/.rubocop_strict.yml +15 -0
  7. data/.version +1 -1
  8. data/CHANGELOG.md +269 -55
  9. data/IMPLEMENTATION_PLAN.md +506 -0
  10. data/README.md +267 -239
  11. data/Rakefile +5 -5
  12. data/_typos.toml +10 -0
  13. data/aia.gemspec +92 -0
  14. data/architecture_review.md +314 -0
  15. data/bin/aia +16 -0
  16. data/config/aia.yml +13 -0
  17. data/docs/AGENTS.md +40 -0
  18. data/docs/advanced-prompting.md +67 -3
  19. data/docs/cli-reference.md +312 -56
  20. data/docs/configuration.md +130 -19
  21. data/docs/contributing.md +56 -2
  22. data/docs/directives-reference.md +593 -78
  23. data/docs/faq.md +85 -3
  24. data/docs/guides/available-models.md +1 -1
  25. data/docs/guides/basic-usage.md +6 -6
  26. data/docs/guides/chat.md +40 -16
  27. data/docs/guides/crew.md +239 -0
  28. data/docs/guides/executable-prompts.md +1 -1
  29. data/docs/guides/index.md +1 -0
  30. data/docs/guides/models.md +15 -0
  31. data/docs/index.md +29 -2
  32. data/docs/installation.md +44 -17
  33. data/docs/mcp-integration.md +40 -0
  34. data/docs/prompt_management.md +85 -86
  35. data/docs/security.md +47 -0
  36. data/docs/special_projects_guide.md +386 -0
  37. data/docs/tools-and-mcp-examples.md +23 -0
  38. data/docs/workflows-and-pipelines.md +84 -7
  39. data/examples/.gitignore +1 -0
  40. data/examples/00_setup_aia.sh +27 -44
  41. data/examples/11_multi_model.sh +4 -14
  42. data/examples/12_token_usage.sh +3 -12
  43. data/examples/18_tools.sh +10 -2
  44. data/examples/22_chat_mode.sh +0 -10
  45. data/examples/23_verify.sh +139 -0
  46. data/examples/24_decompose.sh +139 -0
  47. data/examples/25_spawn.sh +139 -0
  48. data/examples/26_debate.sh +97 -0
  49. data/examples/27_mention_routing.sh +157 -0
  50. data/examples/28_model_switching.sh +106 -0
  51. data/examples/29_agent_harness.sh +177 -0
  52. data/examples/README.md +65 -0
  53. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  54. data/examples/aia_config.yml +1 -1
  55. data/examples/aia_config_orchestrator.yml +45 -0
  56. data/examples/common.sh +18 -6
  57. data/examples/context/tech_stack.md +2 -2
  58. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  59. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  60. data/examples/rules/01_classify_ruby.rb +16 -0
  61. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  62. data/examples/rules/03_gate_prompt_length.rb +19 -0
  63. data/examples/rules/04_tool_selection.rb +41 -0
  64. data/examples/rules/README.md +30 -0
  65. data/examples/run_all.sh +48 -15
  66. data/examples/tools/word_count_tool.rb +1 -1
  67. data/lib/AGENTS.md +57 -0
  68. data/lib/aia/chat_loop.rb +263 -167
  69. data/lib/aia/config/cli_parser.rb +217 -145
  70. data/lib/aia/config/defaults.yml +62 -33
  71. data/lib/aia/config/mcp_parser.rb +52 -51
  72. data/lib/aia/config/model_spec.rb +34 -2
  73. data/lib/aia/config/validator.rb +171 -216
  74. data/lib/aia/config.rb +111 -145
  75. data/lib/aia/content_extractor.rb +155 -0
  76. data/lib/aia/cost_calculator.rb +39 -0
  77. data/lib/aia/crew.rb +164 -0
  78. data/lib/aia/debate_handler.rb +174 -0
  79. data/lib/aia/delegate_handler.rb +116 -0
  80. data/lib/aia/directive.rb +43 -26
  81. data/lib/aia/directive_processor.rb +16 -7
  82. data/lib/aia/directives/configuration_directives.rb +214 -60
  83. data/lib/aia/directives/context_directives.rb +67 -52
  84. data/lib/aia/directives/execution_directives.rb +141 -4
  85. data/lib/aia/directives/model_directives.rb +163 -141
  86. data/lib/aia/directives/trakflow_directives.rb +62 -0
  87. data/lib/aia/directives/utility_directives.rb +227 -30
  88. data/lib/aia/directives/web_and_file_directives.rb +126 -77
  89. data/lib/aia/errors.rb +15 -0
  90. data/lib/aia/fact_asserter.rb +27 -0
  91. data/lib/aia/fzf.rb +9 -31
  92. data/lib/aia/handler_context.rb +17 -0
  93. data/lib/aia/handler_protocol.rb +19 -0
  94. data/lib/aia/history_transfer.rb +55 -0
  95. data/lib/aia/input_collector.rb +3 -3
  96. data/lib/aia/layered_orchestrator.rb +471 -0
  97. data/lib/aia/logger.rb +45 -25
  98. data/lib/aia/mcp_config_normalizer.rb +35 -0
  99. data/lib/aia/mcp_connection_manager.rb +315 -0
  100. data/lib/aia/mcp_discovery.rb +44 -0
  101. data/lib/aia/mcp_grouper.rb +33 -0
  102. data/lib/aia/mcp_server_config.rb +30 -0
  103. data/lib/aia/mcp_utility.rb +60 -0
  104. data/lib/aia/mention_router.rb +217 -0
  105. data/lib/aia/model_alias_registry.rb +97 -0
  106. data/lib/aia/model_switch_handler.rb +100 -0
  107. data/lib/aia/network_builder.rb +160 -0
  108. data/lib/aia/network_memory_manager.rb +55 -0
  109. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  110. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  111. data/lib/aia/pipeline_orchestrator.rb +272 -0
  112. data/lib/aia/plugin_loader.rb +170 -0
  113. data/lib/aia/plugin_monitor.rb +211 -0
  114. data/lib/aia/prompt_decomposer.rb +159 -0
  115. data/lib/aia/prompt_handler.rb +56 -82
  116. data/lib/aia/robot_builder.rb +51 -0
  117. data/lib/aia/robot_factory.rb +338 -0
  118. data/lib/aia/robot_namer.rb +110 -0
  119. data/lib/aia/session.rb +87 -17
  120. data/lib/aia/session_tracker.rb +207 -0
  121. data/lib/aia/similarity_scorer.rb +41 -0
  122. data/lib/aia/skill_utils.rb +105 -1
  123. data/lib/aia/spawn_handler.rb +129 -0
  124. data/lib/aia/spawn_spec_parser.rb +65 -0
  125. data/lib/aia/special_mode_handler.rb +322 -0
  126. data/lib/aia/speech.rb +67 -0
  127. data/lib/aia/startup_coordinator.rb +151 -0
  128. data/lib/aia/streaming_runner.rb +172 -0
  129. data/lib/aia/system_prompt_assembler.rb +92 -0
  130. data/lib/aia/task_coordinator.rb +207 -0
  131. data/lib/aia/task_decomposer.rb +57 -0
  132. data/lib/aia/task_executor.rb +51 -0
  133. data/lib/aia/tfidf_math.rb +27 -0
  134. data/lib/aia/timing.rb +15 -0
  135. data/lib/aia/tool_filter/tfidf.rb +116 -0
  136. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  137. data/lib/aia/tool_filter.rb +83 -0
  138. data/lib/aia/tool_filter_registry.rb +30 -0
  139. data/lib/aia/tool_filter_strategy.rb +146 -0
  140. data/lib/aia/tool_introspection.rb +17 -0
  141. data/lib/aia/tool_loader.rb +216 -0
  142. data/lib/aia/tool_utility.rb +30 -0
  143. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  144. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  145. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  146. data/lib/aia/tools/task_board_tool.rb +115 -0
  147. data/lib/aia/trakflow_bridge.rb +175 -0
  148. data/lib/aia/turn_state.rb +95 -0
  149. data/lib/aia/ui_presenter.rb +182 -206
  150. data/lib/aia/utility.rb +136 -87
  151. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
  152. data/lib/aia/verification_network.rb +57 -0
  153. data/lib/aia.rb +124 -63
  154. data/mkdocs.yml +1 -0
  155. metadata +187 -58
  156. data/justfile +0 -215
  157. data/lib/aia/adapter/chat_execution.rb +0 -242
  158. data/lib/aia/adapter/error_handler.rb +0 -68
  159. data/lib/aia/adapter/gem_activator.rb +0 -57
  160. data/lib/aia/adapter/mcp_connector.rb +0 -274
  161. data/lib/aia/adapter/modality_handlers.rb +0 -167
  162. data/lib/aia/adapter/model_registry.rb +0 -81
  163. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  164. data/lib/aia/adapter/provider_configurator.rb +0 -59
  165. data/lib/aia/adapter/tool_filter.rb +0 -85
  166. data/lib/aia/adapter/tool_loader.rb +0 -90
  167. data/lib/aia/chat_processor_service.rb +0 -178
  168. data/lib/aia/prompt_pipeline.rb +0 -183
  169. data/lib/aia/ruby_llm_adapter.rb +0 -95
  170. data/lib/extensions/openstruct_merge.rb +0 -48
  171. data/lib/extensions/ruby_llm/.irbrc +0 -56
  172. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  173. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  174. data/lib/refinements/string.rb +0 -16
  175. data/main.just +0 -76
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plugin_loader'
4
+
5
+ module AIA
6
+ # Watches the plugins directory and keeps the running process in sync with it:
7
+ #
8
+ # - a new `*.rb` file -> loaded
9
+ # - an existing file changed -> reloaded (old definitions removed first)
10
+ # - a file removed -> its definitions removed from the process
11
+ #
12
+ # Detection prefers the `listen` gem (native OS file events; instant, no idle
13
+ # CPU). If `listen` cannot be loaded, it falls back to a background thread that
14
+ # polls file signatures ([mtime, size]). Either way the same #react logic drives
15
+ # {PluginLoader}, so behavior is identical — only latency differs.
16
+ # :reek:TooManyInstanceVariables -- holds both backends' state (listener vs polling thread) plus watch config and callbacks
17
+ class PluginMonitor
18
+ DEFAULT_INTERVAL = 1.0 # seconds (polling fallback only)
19
+
20
+ # @param config [#paths] provides paths.plugins_dir
21
+ # @param loader [#load_file, #unload_file] plugin loader (default PluginLoader)
22
+ # @param interval [Float] seconds between scans (polling fallback only)
23
+ # @param output [IO] where status messages go (default $stderr)
24
+ def initialize(config, loader: PluginLoader, interval: DEFAULT_INTERVAL, output: $stderr)
25
+ @config = config
26
+ @loader = loader
27
+ @interval = interval
28
+ @output = output
29
+ @dir = config.paths&.plugins_dir
30
+ @thread = nil
31
+ @listener = nil
32
+ @running = false
33
+ @backend = nil
34
+ @snapshot = {}
35
+ end
36
+
37
+ # Start watching. Prefers listen; falls back to polling. No-op if the
38
+ # directory is not watchable or the monitor is already running.
39
+ #
40
+ # @return [self]
41
+ def start
42
+ return self unless watchable?
43
+ return self if @running
44
+
45
+ @snapshot = current_snapshot
46
+ @running = true
47
+ @backend = start_listen ? :listen : start_polling
48
+ self
49
+ end
50
+
51
+ # Stop watching, whichever backend is active.
52
+ #
53
+ # @return [self]
54
+ def stop
55
+ @running = false
56
+ stop_listen
57
+ stop_polling
58
+ @backend = nil
59
+ self
60
+ end
61
+
62
+ # @return [Boolean]
63
+ def running?
64
+ @running
65
+ end
66
+
67
+ # @return [Symbol, nil] :listen or :polling once started
68
+ attr_reader :backend
69
+
70
+ # Perform one scan-and-react cycle (polling backend). Compares the current
71
+ # directory state to the previous snapshot, applies each change, then stores
72
+ # the new snapshot. Public so it can be driven directly in tests.
73
+ #
74
+ # @return [Array<Array(Symbol, String)>] the changes applied this tick
75
+ def tick
76
+ now = current_snapshot
77
+ changes = detect_changes(@snapshot, now)
78
+ changes.each { |event, path| react(event, path) }
79
+ @snapshot = now
80
+ changes
81
+ end
82
+
83
+ # Compare two `path => signature` snapshots and classify the differences.
84
+ #
85
+ # @return [Array<Array(Symbol, String)>] e.g. [[:added, "/p/foo.rb"], ...]
86
+ def detect_changes(old_snapshot, new_snapshot)
87
+ old_keys = old_snapshot.keys
88
+ new_keys = new_snapshot.keys
89
+ added = (new_keys - old_keys).map { |path| [:added, path] }
90
+ removed = (old_keys - new_keys).map { |path| [:removed, path] }
91
+ modified = (old_keys & new_keys)
92
+ .reject { |path| new_snapshot[path] == old_snapshot[path] }
93
+ .map { |path| [:modified, path] }
94
+ added + removed + modified
95
+ end
96
+
97
+ private
98
+
99
+ def watchable?
100
+ !@dir.nil? && !@dir.to_s.strip.empty? && Dir.exist?(@dir)
101
+ end
102
+
103
+ # --- listen backend ---
104
+
105
+ # Try to start the listen-based watcher. Returns true on success, false if
106
+ # the listen gem is unavailable (caller then uses the polling fallback).
107
+ def start_listen
108
+ require 'listen'
109
+ @listener = Listen.to(@dir, only: /\.rb$/) do |modified, added, removed|
110
+ handle_listen_events(modified, added, removed)
111
+ end
112
+ @listener.start
113
+ true
114
+ rescue LoadError
115
+ false
116
+ end
117
+
118
+ # Map a listen callback into per-file reactions, ignoring anything that is
119
+ # not a top-level file of the plugins directory (listen watches recursively).
120
+ def handle_listen_events(modified, added, removed)
121
+ { added: added, modified: modified, removed: removed }.each do |event, paths|
122
+ Array(paths).each { |path| react(event, path) if top_level?(path) }
123
+ end
124
+ end
125
+
126
+ # listen reports resolved (symlink-free) paths, and on macOS the plugins
127
+ # dir is often reached via a symlink (e.g. /tmp -> /private/tmp). Compare
128
+ # canonicalized directories so the top-level filter matches correctly.
129
+ def top_level?(path)
130
+ canonical_dir(File.dirname(path)) == watched_dir
131
+ end
132
+
133
+ def watched_dir
134
+ @watched_dir ||= canonical_dir(@dir)
135
+ end
136
+
137
+ def canonical_dir(dir)
138
+ File.realpath(dir)
139
+ rescue StandardError
140
+ File.expand_path(dir)
141
+ end
142
+
143
+ def stop_listen
144
+ @listener&.stop
145
+ rescue StandardError
146
+ # best-effort
147
+ ensure
148
+ @listener = nil
149
+ end
150
+
151
+ # --- polling backend ---
152
+
153
+ def start_polling
154
+ @thread = Thread.new { watch_loop }
155
+ @thread.name = 'aia-plugin-monitor' if @thread.respond_to?(:name=)
156
+ :polling
157
+ end
158
+
159
+ def stop_polling
160
+ begin
161
+ @thread&.wakeup
162
+ rescue ThreadError
163
+ # thread already finished
164
+ end
165
+ @thread&.join(@interval + 0.5)
166
+ @thread = nil
167
+ end
168
+
169
+ def watch_loop
170
+ while @running
171
+ begin
172
+ tick
173
+ rescue StandardError => e
174
+ @output.puts "Plugin monitor error: #{e.message}"
175
+ end
176
+ sleep @interval
177
+ end
178
+ end
179
+
180
+ # --- shared ---
181
+
182
+ # @return [Hash{String=>Array}] path => [mtime, size]
183
+ def current_snapshot
184
+ Dir.glob(File.join(@dir, '*.rb')).to_h { |path| [path, file_signature(path)] }
185
+ end
186
+
187
+ def file_signature(path)
188
+ stat = File.stat(path)
189
+ [stat.mtime, stat.size]
190
+ rescue SystemCallError
191
+ nil
192
+ end
193
+
194
+ # Apply a single change by driving the loader, and report it.
195
+ def react(event, path)
196
+ name = File.basename(path, '.rb')
197
+ case event
198
+ when :added
199
+ announce('loaded', name) if @loader.load_file(path, @config)
200
+ when :modified
201
+ announce('reloaded', name) if @loader.load_file(path, @config)
202
+ when :removed
203
+ announce('unloaded', name) if @loader.unload_file(name, @config)
204
+ end
205
+ end
206
+
207
+ def announce(action, name)
208
+ @output.puts "Plugin #{action}: #{name}"
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/prompt_decomposer.rb
4
+ #
5
+ # Decomposes complex prompts into independent sub-tasks.
6
+ # A coordinator robot breaks them down, specialist robots execute,
7
+ # and the coordinator reassembles.
8
+
9
+ require 'json'
10
+
11
+ module AIA
12
+ class PromptDecomposer
13
+ include ContentExtractor
14
+
15
+ DECOMPOSITION_PROMPT = <<~PROMPT
16
+ Analyze this user request and determine if it can be broken into independent sub-tasks.
17
+
18
+ Rules:
19
+ - Only decompose if sub-tasks are truly independent (can run in parallel)
20
+ - Each sub-task should be self-contained
21
+ - 2-5 sub-tasks maximum
22
+ - Keep sub-task descriptions clear and specific
23
+ - Use an empty subtasks array if the request cannot be meaningfully decomposed
24
+
25
+ User request: %{prompt}
26
+ PROMPT
27
+
28
+ # Appended to DECOMPOSITION_PROMPT when the model does not support
29
+ # structured output and with_schema cannot be used.
30
+ FALLBACK_JSON_INSTRUCTION = <<~INSTRUCTION
31
+
32
+ Respond with ONLY a JSON object — no explanation, no markdown, no prose:
33
+ {"subtasks": ["sub-task description 1", "sub-task description 2"]}
34
+ INSTRUCTION
35
+
36
+ # JSON schema for structured-output-capable models.
37
+ # Uses a wrapper object because Claude requires a top-level object.
38
+ SUBTASKS_SCHEMA = {
39
+ name: 'subtask_decomposition',
40
+ schema: {
41
+ type: 'object',
42
+ properties: {
43
+ subtasks: {
44
+ type: 'array',
45
+ items: { type: 'string' },
46
+ description: 'Independent sub-task descriptions. Empty array if not decomposable.'
47
+ }
48
+ },
49
+ required: ['subtasks'],
50
+ additionalProperties: false
51
+ },
52
+ strict: true
53
+ }.freeze
54
+
55
+ SYNTHESIS_TEMPLATE = <<~PROMPT
56
+ Original request: %{prompt}
57
+
58
+ Sub-task results:
59
+ %{results}
60
+
61
+ Synthesize these results into a coherent final response that fully addresses the original request.
62
+ PROMPT
63
+
64
+ def initialize(robot)
65
+ @robot = robot
66
+ end
67
+
68
+ # Attempt to decompose a prompt into sub-tasks.
69
+ #
70
+ # Uses a temporary probe robot so the decomposition prompt and response do
71
+ # not pollute the main conversation history. When the configured model
72
+ # supports structured output (structured_output? == true) with_schema is
73
+ # used to get a guaranteed Hash back. Otherwise a JSON instruction is
74
+ # appended to the prompt and the text response is parsed manually.
75
+ #
76
+ # @param prompt [String] the user's prompt
77
+ # @return [Array<String>] sub-task descriptions (empty if not decomposable)
78
+ # :reek:TooManyStatements -- schema vs JSON-fallback probe setup, run, extraction, and rescue reporting in one flow
79
+ def decompose(prompt)
80
+ logger = AIA.logger
81
+ probe = build_probe_robot
82
+ use_schema = structured_output?
83
+
84
+ if use_schema
85
+ probe.with_schema(SUBTASKS_SCHEMA)
86
+ full_prompt = DECOMPOSITION_PROMPT % { prompt: prompt }
87
+ else
88
+ logger.warn("PromptDecomposer: configured model does not support structured output — using JSON prompt fallback")
89
+ full_prompt = (DECOMPOSITION_PROMPT + FALLBACK_JSON_INSTRUCTION) % { prompt: prompt }
90
+ end
91
+
92
+ result = probe.run(full_prompt, mcp: :none, tools: :none)
93
+ content = extract_content(result)
94
+ logger.debug("PromptDecomposer#decompose content class=#{content.class}")
95
+ subtasks = extract_subtasks(content)
96
+ subtasks.is_a?(Array) ? subtasks.select { |t| t.is_a?(String) && !t.empty? } : []
97
+ rescue StandardError => e
98
+ logger.warn("PromptDecomposer#decompose failed: #{e.class}: #{e.message}")
99
+ []
100
+ end
101
+
102
+ # Synthesize sub-task results into a final response.
103
+ #
104
+ # @param prompt [String] the original user prompt
105
+ # @param results [Array<String>] results from each sub-task
106
+ # @return the synthesized response
107
+ def synthesize(prompt, results)
108
+ formatted_results = results.each_with_index.map do |r, i|
109
+ "Sub-task #{i + 1}:\n#{r}"
110
+ end.join("\n\n")
111
+
112
+ @robot.run(
113
+ SYNTHESIS_TEMPLATE % { prompt: prompt, results: formatted_results },
114
+ mcp: :none, tools: :none
115
+ )
116
+ end
117
+
118
+ private
119
+
120
+ def structured_output?
121
+ RubyLLM.models.find(AIA.config.models.first.internal_id).structured_output?
122
+ rescue StandardError
123
+ false
124
+ end
125
+
126
+ # Build a fresh temporary robot for the decomposition probe.
127
+ # This robot has no conversation history and is discarded after use,
128
+ # keeping @robot's history clean for the normal or synthesis path.
129
+ def build_probe_robot
130
+ config = AIA.config
131
+ run_config = RobotFactory.build_run_config(config)
132
+ RobotLab.build(
133
+ name: "decompose-probe",
134
+ model: config.models.first.internal_id,
135
+ system_prompt: nil,
136
+ config: run_config
137
+ )
138
+ end
139
+
140
+ # Extract the subtasks array from the probe response.
141
+ # Hash path: with_schema caused ruby_llm to auto-parse the JSON response.
142
+ # String path: fallback text parsing for models without structured output.
143
+ def extract_subtasks(content)
144
+ case content
145
+ when Hash
146
+ content['subtasks'] || content[:subtasks] || []
147
+ when Array
148
+ content
149
+ when String
150
+ parsed = JSON.parse(content.gsub(/```(?:json)?\s*/i, '').gsub('```', '').strip)
151
+ parsed.is_a?(Hash) ? parsed['subtasks'] || parsed[:subtasks] || [] : parsed
152
+ else
153
+ []
154
+ end
155
+ rescue JSON::ParserError
156
+ []
157
+ end
158
+ end
159
+ end