agentic 0.1.0 → 0.2.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/.agentic.yml +2 -0
- data/.architecture/decisions/ArchitecturalFeatureBuilder.md +136 -0
- data/.architecture/decisions/ArchitectureConsiderations.md +200 -0
- data/.architecture/decisions/adr_001_observer_pattern_implementation.md +196 -0
- data/.architecture/decisions/adr_002_plan_orchestrator.md +320 -0
- data/.architecture/decisions/adr_003_plan_orchestrator_interface.md +179 -0
- data/.architecture/decisions/adrs/ADR-001-dependency-management.md +147 -0
- data/.architecture/decisions/adrs/ADR-002-system-boundaries.md +162 -0
- data/.architecture/decisions/adrs/ADR-003-content-safety.md +158 -0
- data/.architecture/decisions/adrs/ADR-004-agent-permissions.md +161 -0
- data/.architecture/decisions/adrs/ADR-005-adaptation-engine.md +127 -0
- data/.architecture/decisions/adrs/ADR-006-extension-system.md +273 -0
- data/.architecture/decisions/adrs/ADR-007-learning-system.md +156 -0
- data/.architecture/decisions/adrs/ADR-008-prompt-generation.md +325 -0
- data/.architecture/decisions/adrs/ADR-009-task-failure-handling.md +353 -0
- data/.architecture/decisions/adrs/ADR-010-task-input-handling.md +251 -0
- data/.architecture/decisions/adrs/ADR-011-task-observable-pattern.md +391 -0
- data/.architecture/decisions/adrs/ADR-012-task-output-handling.md +205 -0
- data/.architecture/decisions/adrs/ADR-013-architecture-alignment.md +211 -0
- data/.architecture/decisions/adrs/ADR-014-agent-capability-registry.md +80 -0
- data/.architecture/decisions/adrs/ADR-015-persistent-agent-store.md +100 -0
- data/.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md +117 -0
- data/.architecture/decisions/adrs/ADR-017-streaming-observability.md +171 -0
- data/.architecture/decisions/capability_tools_distinction.md +150 -0
- data/.architecture/decisions/cli_command_structure.md +61 -0
- data/.architecture/implementation/agent_self_assembly_implementation.md +267 -0
- data/.architecture/implementation/agent_self_assembly_summary.md +138 -0
- data/.architecture/members.yml +187 -0
- data/.architecture/planning/self_implementation_exercise.md +295 -0
- data/.architecture/planning/session_compaction_rule.md +43 -0
- data/.architecture/planning/streaming_observability_feature.md +223 -0
- data/.architecture/principles.md +151 -0
- data/.architecture/recalibration/0-2-0.md +92 -0
- data/.architecture/recalibration/agent_self_assembly.md +238 -0
- data/.architecture/recalibration/cli_command_structure.md +91 -0
- data/.architecture/recalibration/implementation_roadmap_0-2-0.md +301 -0
- data/.architecture/recalibration/progress_tracking_0-2-0.md +114 -0
- data/.architecture/recalibration_process.md +127 -0
- data/.architecture/reviews/0-2-0.md +181 -0
- data/.architecture/reviews/cli_command_duplication.md +98 -0
- data/.architecture/templates/adr.md +105 -0
- data/.architecture/templates/implementation_roadmap.md +125 -0
- data/.architecture/templates/progress_tracking.md +89 -0
- data/.architecture/templates/recalibration_plan.md +70 -0
- data/.architecture/templates/version_comparison.md +124 -0
- data/.claude/settings.local.json +13 -0
- data/.claude-sessions/001-task-class-architecture-implementation.md +129 -0
- data/.claude-sessions/002-plan-orchestrator-interface-review.md +105 -0
- data/.claude-sessions/architecture-governance-implementation.md +37 -0
- data/.claude-sessions/architecture-review-session.md +27 -0
- data/ArchitecturalFeatureBuilder.md +136 -0
- data/ArchitectureConsiderations.md +229 -0
- data/CHANGELOG.md +57 -2
- data/CLAUDE.md +111 -0
- data/CONTRIBUTING.md +286 -0
- data/MAINTAINING.md +301 -0
- data/README.md +582 -28
- data/docs/agent_capabilities_api.md +259 -0
- data/docs/artifact_extension_points.md +757 -0
- data/docs/artifact_generation_architecture.md +323 -0
- data/docs/artifact_implementation_plan.md +596 -0
- data/docs/artifact_integration_points.md +345 -0
- data/docs/artifact_verification_strategies.md +581 -0
- data/docs/streaming_observability_architecture.md +510 -0
- data/exe/agentic +6 -1
- data/lefthook.yml +5 -0
- data/lib/agentic/adaptation_engine.rb +124 -0
- data/lib/agentic/agent.rb +181 -4
- data/lib/agentic/agent_assembly_engine.rb +442 -0
- data/lib/agentic/agent_capability_registry.rb +260 -0
- data/lib/agentic/agent_config.rb +63 -0
- data/lib/agentic/agent_specification.rb +46 -0
- data/lib/agentic/capabilities/examples.rb +530 -0
- data/lib/agentic/capabilities.rb +14 -0
- data/lib/agentic/capability_provider.rb +146 -0
- data/lib/agentic/capability_specification.rb +118 -0
- data/lib/agentic/cli/agent.rb +31 -0
- data/lib/agentic/cli/capabilities.rb +191 -0
- data/lib/agentic/cli/config.rb +134 -0
- data/lib/agentic/cli/execution_observer.rb +796 -0
- data/lib/agentic/cli.rb +1068 -0
- data/lib/agentic/default_agent_provider.rb +35 -0
- data/lib/agentic/errors/llm_error.rb +184 -0
- data/lib/agentic/execution_plan.rb +53 -0
- data/lib/agentic/execution_result.rb +91 -0
- data/lib/agentic/expected_answer_format.rb +46 -0
- data/lib/agentic/extension/domain_adapter.rb +109 -0
- data/lib/agentic/extension/plugin_manager.rb +163 -0
- data/lib/agentic/extension/protocol_handler.rb +116 -0
- data/lib/agentic/extension.rb +45 -0
- data/lib/agentic/factory_methods.rb +9 -1
- data/lib/agentic/generation_stats.rb +61 -0
- data/lib/agentic/learning/README.md +84 -0
- data/lib/agentic/learning/capability_optimizer.rb +613 -0
- data/lib/agentic/learning/execution_history_store.rb +251 -0
- data/lib/agentic/learning/pattern_recognizer.rb +500 -0
- data/lib/agentic/learning/strategy_optimizer.rb +706 -0
- data/lib/agentic/learning.rb +131 -0
- data/lib/agentic/llm_assisted_composition_strategy.rb +188 -0
- data/lib/agentic/llm_client.rb +215 -15
- data/lib/agentic/llm_config.rb +65 -1
- data/lib/agentic/llm_response.rb +163 -0
- data/lib/agentic/logger.rb +1 -1
- data/lib/agentic/observable.rb +51 -0
- data/lib/agentic/persistent_agent_store.rb +385 -0
- data/lib/agentic/plan_execution_result.rb +129 -0
- data/lib/agentic/plan_orchestrator.rb +464 -0
- data/lib/agentic/plan_orchestrator_config.rb +57 -0
- data/lib/agentic/retry_config.rb +63 -0
- data/lib/agentic/retry_handler.rb +125 -0
- data/lib/agentic/structured_outputs.rb +1 -1
- data/lib/agentic/task.rb +193 -0
- data/lib/agentic/task_definition.rb +39 -0
- data/lib/agentic/task_execution_result.rb +92 -0
- data/lib/agentic/task_failure.rb +66 -0
- data/lib/agentic/task_output_schemas.rb +112 -0
- data/lib/agentic/task_planner.rb +54 -19
- data/lib/agentic/task_result.rb +48 -0
- data/lib/agentic/ui.rb +244 -0
- data/lib/agentic/verification/critic_framework.rb +116 -0
- data/lib/agentic/verification/llm_verification_strategy.rb +60 -0
- data/lib/agentic/verification/schema_verification_strategy.rb +47 -0
- data/lib/agentic/verification/verification_hub.rb +62 -0
- data/lib/agentic/verification/verification_result.rb +50 -0
- data/lib/agentic/verification/verification_strategy.rb +26 -0
- data/lib/agentic/version.rb +1 -1
- data/lib/agentic.rb +74 -2
- data/plugins/README.md +41 -0
- metadata +245 -6
data/lib/agentic/cli.rb
ADDED
@@ -0,0 +1,1068 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "json"
|
5
|
+
require "yaml"
|
6
|
+
require_relative "cli/capabilities"
|
7
|
+
|
8
|
+
module Agentic
|
9
|
+
# Command Line Interface for Agentic
|
10
|
+
class CLI < Thor
|
11
|
+
class_option :verbose, type: :boolean, aliases: "-v", desc: "Enable verbose output"
|
12
|
+
class_option :quiet, type: :boolean, aliases: "-q", desc: "Suppress output"
|
13
|
+
class_option :config, type: :string, aliases: "-c", desc: "Specify config file"
|
14
|
+
|
15
|
+
def self.exit_on_failure?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
# Global logging configuration based on options
|
20
|
+
def initialize(*args)
|
21
|
+
super
|
22
|
+
configure_logging
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "version", "Display version information"
|
26
|
+
def version
|
27
|
+
version_box = UI.box(
|
28
|
+
"Agentic",
|
29
|
+
[
|
30
|
+
"Version: #{UI.colorize(Agentic::VERSION, :green)}",
|
31
|
+
"Ruby: #{UI.colorize(RUBY_VERSION, :blue)}",
|
32
|
+
"Platform: #{UI.colorize(RUBY_PLATFORM, :yellow)}"
|
33
|
+
].join("\n"),
|
34
|
+
padding: [1, 2, 1, 2],
|
35
|
+
style: {border: {fg: :blue}}
|
36
|
+
)
|
37
|
+
puts version_box
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "plan GOAL", "Create an execution plan for a goal"
|
41
|
+
long_desc <<-LONGDESC
|
42
|
+
Creates an execution plan for the given goal using the TaskPlanner.
|
43
|
+
|
44
|
+
Example:
|
45
|
+
$ agentic plan "Generate a market research report on AI trends"
|
46
|
+
|
47
|
+
You can also save the plan to a file:
|
48
|
+
$ agentic plan "Generate a market research report" --save plan.json
|
49
|
+
LONGDESC
|
50
|
+
option :output, type: :string, aliases: "-o",
|
51
|
+
enum: %w[json yaml text], default: "text",
|
52
|
+
desc: "Output format (json, yaml, or text)"
|
53
|
+
option :save, type: :string, aliases: "-s",
|
54
|
+
desc: "Save plan to a file (defaults to plan-TIMESTAMP.json)"
|
55
|
+
option :model, type: :string, aliases: "-m",
|
56
|
+
desc: "LLM model to use (defaults to configuration)"
|
57
|
+
option :no_interactive, type: :boolean,
|
58
|
+
desc: "Skip interactive plan adjustment prompt"
|
59
|
+
option :execute, type: :boolean,
|
60
|
+
desc: "Execute the plan immediately after generation"
|
61
|
+
def plan(goal)
|
62
|
+
check_api_token!
|
63
|
+
|
64
|
+
say UI.colorize("Creating plan for goal: #{goal}", :green) unless options[:quiet]
|
65
|
+
|
66
|
+
# Configure the LLM
|
67
|
+
config = LlmConfig.new
|
68
|
+
config.model = options[:model] if options[:model]
|
69
|
+
|
70
|
+
# Create and run the task planner with spinner
|
71
|
+
execution_plan = UI.with_spinner("Planning tasks for goal", quiet: options[:quiet]) do
|
72
|
+
planner = TaskPlanner.new(goal, config)
|
73
|
+
planner.plan
|
74
|
+
end
|
75
|
+
|
76
|
+
# Show the plan to the user
|
77
|
+
unless options[:quiet]
|
78
|
+
puts format_plan(execution_plan)
|
79
|
+
puts
|
80
|
+
end
|
81
|
+
|
82
|
+
# Ask user if they want to adjust the plan
|
83
|
+
if !options[:quiet] && !options[:no_interactive] && ask_user_for_plan_adjustment
|
84
|
+
execution_plan = adjust_plan_with_user_input(execution_plan, config)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Output the plan based on format option (only if saving to file or different format)
|
88
|
+
if options[:save] || options[:output] != "text"
|
89
|
+
output_plan(execution_plan, options)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Ask user if they want to execute the plan
|
93
|
+
if options[:execute] || (should_ask_for_execution? && ask_user_for_execution)
|
94
|
+
execute_plan_immediately(execution_plan)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
desc "execute", "Execute a plan"
|
99
|
+
long_desc <<-LONGDESC
|
100
|
+
Executes a plan created by the 'plan' command.
|
101
|
+
|
102
|
+
You can provide a plan file:
|
103
|
+
$ agentic execute --plan plan.json
|
104
|
+
|
105
|
+
Or pipe in a plan:
|
106
|
+
$ cat plan.json | agentic execute --from-stdin
|
107
|
+
LONGDESC
|
108
|
+
option :plan, type: :string, aliases: "-p",
|
109
|
+
desc: "Path to a plan file"
|
110
|
+
option :from_stdin, type: :boolean,
|
111
|
+
desc: "Read plan from stdin"
|
112
|
+
option :async, type: :boolean, default: true,
|
113
|
+
desc: "Execute tasks asynchronously"
|
114
|
+
option :max_concurrency, type: :numeric, default: 10,
|
115
|
+
desc: "Maximum concurrent tasks"
|
116
|
+
option :file, type: :string, aliases: "-f",
|
117
|
+
desc: "Output file path (defaults to result-TIMESTAMP.json)"
|
118
|
+
option :model, type: :string, aliases: "-m",
|
119
|
+
desc: "LLM model to use (defaults to configuration)"
|
120
|
+
def execute
|
121
|
+
check_api_token!
|
122
|
+
|
123
|
+
# Load the plan
|
124
|
+
plan_data = load_plan_data
|
125
|
+
|
126
|
+
unless plan_data
|
127
|
+
raise Thor::Error, "No plan provided. Use --plan FILE or --from-stdin"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Initialize task instances from the plan
|
131
|
+
tasks = initialize_tasks(plan_data)
|
132
|
+
|
133
|
+
# Execute the tasks
|
134
|
+
execute_tasks(tasks)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Agent commands
|
138
|
+
class AgentCommands < Thor
|
139
|
+
desc "list", "List available agents"
|
140
|
+
option :detailed, type: :boolean, aliases: "-d",
|
141
|
+
desc: "Show detailed information"
|
142
|
+
def list
|
143
|
+
# Initialize agent assembly system
|
144
|
+
Agentic.initialize_agent_assembly
|
145
|
+
|
146
|
+
# Get stored agents
|
147
|
+
agents = Agentic.agent_store.all
|
148
|
+
|
149
|
+
if agents.empty?
|
150
|
+
puts UI.box(
|
151
|
+
"Available Agents",
|
152
|
+
"No agents stored yet.\n\n" \
|
153
|
+
"You can create a new agent with:\n" \
|
154
|
+
" #{UI.colorize("agentic agent create NAME --role=ROLE --purpose=PURPOSE", :blue)}",
|
155
|
+
padding: [1, 2, 1, 2],
|
156
|
+
style: {border: {fg: :blue}}
|
157
|
+
)
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
output = ""
|
162
|
+
agents.each do |agent|
|
163
|
+
output += "#{UI.colorize(agent[:name], :blue)} (#{agent[:id]}):\n"
|
164
|
+
output += " Stored: #{agent[:timestamp]}\n"
|
165
|
+
|
166
|
+
if options[:detailed]
|
167
|
+
output += " Role: #{agent[:agent][:role]}\n" if agent[:agent] && agent[:agent][:role]
|
168
|
+
output += " Purpose: #{agent[:agent][:purpose]}\n" if agent[:agent] && agent[:agent][:purpose]
|
169
|
+
|
170
|
+
if agent[:capabilities] && !agent[:capabilities].empty?
|
171
|
+
output += " Capabilities:\n"
|
172
|
+
agent[:capabilities].each do |capability|
|
173
|
+
output += " - #{capability[:name]} (v#{capability[:version]})\n"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
if agent[:metadata] && !agent[:metadata].empty?
|
178
|
+
output += " Metadata:\n"
|
179
|
+
agent[:metadata].each do |key, value|
|
180
|
+
output += " - #{key}: #{value}\n" unless key.to_s == "requirements"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
else
|
184
|
+
capabilities_count = agent[:capabilities] ? agent[:capabilities].size : 0
|
185
|
+
output += " Capabilities: #{capabilities_count}\n"
|
186
|
+
end
|
187
|
+
|
188
|
+
output += "\n"
|
189
|
+
end
|
190
|
+
|
191
|
+
puts UI.box(
|
192
|
+
"Available Agents",
|
193
|
+
output,
|
194
|
+
padding: [1, 2, 1, 2],
|
195
|
+
style: {border: {fg: :blue}}
|
196
|
+
)
|
197
|
+
end
|
198
|
+
|
199
|
+
desc "create NAME", "Create a new agent"
|
200
|
+
option :role, type: :string, required: true, desc: "Role of the agent"
|
201
|
+
option :purpose, type: :string, required: true, desc: "Purpose of the agent"
|
202
|
+
option :backstory, type: :string, desc: "Backstory for the agent"
|
203
|
+
option :capabilities, type: :array, desc: "Capabilities to add to the agent"
|
204
|
+
def create(name)
|
205
|
+
# Initialize agent assembly system
|
206
|
+
Agentic.initialize_agent_assembly
|
207
|
+
|
208
|
+
# Create spinner for agent creation
|
209
|
+
agent = UI.with_spinner("Creating agent: #{name}") do
|
210
|
+
# Create new agent
|
211
|
+
agent = Agentic::Agent.new do |a|
|
212
|
+
a.role = options[:role]
|
213
|
+
a.purpose = options[:purpose]
|
214
|
+
a.backstory = options[:backstory] || ""
|
215
|
+
end
|
216
|
+
|
217
|
+
# Add capabilities if specified
|
218
|
+
options[:capabilities]&.each do |capability_name|
|
219
|
+
agent.add_capability(capability_name)
|
220
|
+
rescue => e
|
221
|
+
Agentic.logger.warn("Failed to add capability: #{capability_name} - #{e.message}")
|
222
|
+
end
|
223
|
+
|
224
|
+
# Store the agent
|
225
|
+
Agentic.agent_store.store(agent, name: name)
|
226
|
+
|
227
|
+
agent
|
228
|
+
end
|
229
|
+
|
230
|
+
# Format capabilities list
|
231
|
+
capabilities = agent.capabilities.keys.map { |c| "- #{c}" }.join("\n")
|
232
|
+
capabilities = "None" if capabilities.empty?
|
233
|
+
|
234
|
+
# Show success message with agent details
|
235
|
+
details = [
|
236
|
+
"Name: #{UI.colorize(name, :blue)}",
|
237
|
+
"Role: #{UI.colorize(agent.role, :magenta)}",
|
238
|
+
"Purpose: #{agent.purpose}",
|
239
|
+
"Capabilities:\n#{capabilities}"
|
240
|
+
].join("\n")
|
241
|
+
|
242
|
+
puts UI.box(
|
243
|
+
"Agent Created",
|
244
|
+
details,
|
245
|
+
padding: [1, 2, 1, 2],
|
246
|
+
style: {border: {fg: :green}}
|
247
|
+
)
|
248
|
+
end
|
249
|
+
|
250
|
+
desc "show ID_OR_NAME", "Show details of a specific agent"
|
251
|
+
def show(id_or_name)
|
252
|
+
# Initialize agent assembly system
|
253
|
+
Agentic.initialize_agent_assembly
|
254
|
+
|
255
|
+
# Find the agent
|
256
|
+
agent_config = nil
|
257
|
+
Agentic.agent_store.all.each do |config|
|
258
|
+
if config[:id] == id_or_name || config[:name] == id_or_name
|
259
|
+
agent_config = config
|
260
|
+
break
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
unless agent_config
|
265
|
+
puts UI.box(
|
266
|
+
"Error",
|
267
|
+
"Agent '#{UI.colorize(id_or_name, :yellow)}' not found.",
|
268
|
+
padding: [1, 2, 1, 2],
|
269
|
+
style: {border: {fg: :red}}
|
270
|
+
)
|
271
|
+
exit 1
|
272
|
+
end
|
273
|
+
|
274
|
+
# Format the agent details
|
275
|
+
output = ""
|
276
|
+
output += "ID: #{UI.colorize(agent_config[:id], :blue)}\n"
|
277
|
+
output += "Name: #{UI.colorize(agent_config[:name], :blue)}\n"
|
278
|
+
output += "Stored: #{agent_config[:timestamp]}\n"
|
279
|
+
output += "Version: #{agent_config[:version]}\n\n"
|
280
|
+
|
281
|
+
if agent_config[:agent]
|
282
|
+
output += "Role: #{agent_config[:agent][:role]}\n" if agent_config[:agent][:role]
|
283
|
+
output += "Purpose: #{agent_config[:agent][:purpose]}\n" if agent_config[:agent][:purpose]
|
284
|
+
output += "Backstory: #{agent_config[:agent][:backstory]}\n" if agent_config[:agent][:backstory]
|
285
|
+
output += "\n"
|
286
|
+
end
|
287
|
+
|
288
|
+
if agent_config[:capabilities] && !agent_config[:capabilities].empty?
|
289
|
+
output += "Capabilities:\n"
|
290
|
+
agent_config[:capabilities].each do |capability|
|
291
|
+
output += " - #{UI.colorize(capability[:name], :magenta)} (v#{capability[:version]})\n"
|
292
|
+
end
|
293
|
+
output += "\n"
|
294
|
+
end
|
295
|
+
|
296
|
+
if agent_config[:metadata] && !agent_config[:metadata].empty?
|
297
|
+
output += "Metadata:\n"
|
298
|
+
agent_config[:metadata].each do |key, value|
|
299
|
+
next if key.to_s == "requirements" # Skip complex requirements object
|
300
|
+
output += " - #{key}: #{value}\n"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
puts UI.box(
|
305
|
+
"Agent Details",
|
306
|
+
output,
|
307
|
+
padding: [1, 2, 1, 2],
|
308
|
+
style: {border: {fg: :blue}}
|
309
|
+
)
|
310
|
+
end
|
311
|
+
|
312
|
+
desc "delete ID_OR_NAME", "Delete an agent"
|
313
|
+
def delete(id_or_name)
|
314
|
+
# Initialize agent assembly system
|
315
|
+
Agentic.initialize_agent_assembly
|
316
|
+
|
317
|
+
# Create spinner for agent deletion
|
318
|
+
success = UI.with_spinner("Deleting agent: #{id_or_name}") do
|
319
|
+
Agentic.agent_store.delete(id_or_name)
|
320
|
+
end
|
321
|
+
|
322
|
+
if success
|
323
|
+
puts UI.box(
|
324
|
+
"Agent Deleted",
|
325
|
+
"Agent #{UI.colorize(id_or_name, :blue)} has been deleted successfully.",
|
326
|
+
padding: [1, 2, 1, 2],
|
327
|
+
style: {border: {fg: :yellow}}
|
328
|
+
)
|
329
|
+
else
|
330
|
+
puts UI.box(
|
331
|
+
"Error",
|
332
|
+
"Agent #{UI.colorize(id_or_name, :blue)} could not be deleted. Please check the ID or name.",
|
333
|
+
padding: [1, 2, 1, 2],
|
334
|
+
style: {border: {fg: :red}}
|
335
|
+
)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
desc "build ID_OR_NAME", "Build an agent from storage"
|
340
|
+
def build(id_or_name)
|
341
|
+
# Initialize agent assembly system
|
342
|
+
Agentic.initialize_agent_assembly
|
343
|
+
|
344
|
+
# Build the agent
|
345
|
+
agent = UI.with_spinner("Building agent: #{id_or_name}") do
|
346
|
+
Agentic.agent_store.build_agent(id_or_name)
|
347
|
+
end
|
348
|
+
|
349
|
+
unless agent
|
350
|
+
puts UI.box(
|
351
|
+
"Error",
|
352
|
+
"Agent '#{UI.colorize(id_or_name, :yellow)}' not found or could not be built.",
|
353
|
+
padding: [1, 2, 1, 2],
|
354
|
+
style: {border: {fg: :red}}
|
355
|
+
)
|
356
|
+
exit 1
|
357
|
+
end
|
358
|
+
|
359
|
+
# Format capabilities list
|
360
|
+
capabilities = agent.capabilities.keys.map { |c| "- #{c}" }.join("\n")
|
361
|
+
capabilities = "None" if capabilities.empty?
|
362
|
+
|
363
|
+
# Show success message with agent details
|
364
|
+
details = [
|
365
|
+
"Role: #{UI.colorize(agent.role, :magenta)}",
|
366
|
+
"Purpose: #{agent.purpose}",
|
367
|
+
"Capabilities:\n#{capabilities}"
|
368
|
+
].join("\n")
|
369
|
+
|
370
|
+
puts UI.box(
|
371
|
+
"Agent Built Successfully",
|
372
|
+
details,
|
373
|
+
padding: [1, 2, 1, 2],
|
374
|
+
style: {border: {fg: :green}}
|
375
|
+
)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
desc "agent", "Manage agents"
|
380
|
+
subcommand "agent", AgentCommands
|
381
|
+
|
382
|
+
# Configuration commands
|
383
|
+
class ConfigCommands < Thor
|
384
|
+
desc "list", "List configuration settings"
|
385
|
+
CONFIG_FILE_NAME = ".agentic.yml"
|
386
|
+
USER_CONFIG_PATH = File.join(Dir.home, CONFIG_FILE_NAME)
|
387
|
+
PROJECT_CONFIG_PATH = File.join(Dir.pwd, CONFIG_FILE_NAME)
|
388
|
+
|
389
|
+
desc "list", "List configuration settings"
|
390
|
+
def list
|
391
|
+
user_config = load_config(USER_CONFIG_PATH)
|
392
|
+
project_config = load_config(PROJECT_CONFIG_PATH)
|
393
|
+
|
394
|
+
# Format user config
|
395
|
+
user_config_str = "User configuration (#{USER_CONFIG_PATH}):\n"
|
396
|
+
user_config_str += format_config(user_config)
|
397
|
+
|
398
|
+
# Format project config
|
399
|
+
project_config_str = "Project configuration (#{PROJECT_CONFIG_PATH}):\n"
|
400
|
+
project_config_str += format_config(project_config)
|
401
|
+
|
402
|
+
# Format active config
|
403
|
+
active_config_str = "Active configuration:\n"
|
404
|
+
active_config_str += format_config(active_config)
|
405
|
+
|
406
|
+
# Format environment variables
|
407
|
+
env_vars_str = "Environment variables:\n"
|
408
|
+
token_status = ENV["OPENAI_ACCESS_TOKEN"] ?
|
409
|
+
UI.colorize("[SET]", :green) :
|
410
|
+
UI.colorize("[NOT SET]", :red)
|
411
|
+
env_vars_str += " OPENAI_ACCESS_TOKEN: #{token_status}"
|
412
|
+
|
413
|
+
# Display in a box
|
414
|
+
config_info = [
|
415
|
+
user_config_str,
|
416
|
+
"",
|
417
|
+
project_config_str,
|
418
|
+
"",
|
419
|
+
active_config_str,
|
420
|
+
"",
|
421
|
+
env_vars_str
|
422
|
+
].join("\n")
|
423
|
+
|
424
|
+
puts UI.box("Configuration", config_info, padding: [1, 2, 1, 2], style: {border: {fg: :blue}})
|
425
|
+
end
|
426
|
+
|
427
|
+
desc "get KEY", "Get a configuration setting"
|
428
|
+
def get(key)
|
429
|
+
config = active_config
|
430
|
+
value = config[key]
|
431
|
+
|
432
|
+
if value
|
433
|
+
value_str = case value
|
434
|
+
when true then UI.colorize("true", :green)
|
435
|
+
when false then UI.colorize("false", :red)
|
436
|
+
when String then "\"#{value}\""
|
437
|
+
else value.to_s
|
438
|
+
end
|
439
|
+
|
440
|
+
puts UI.box(
|
441
|
+
"Configuration Value",
|
442
|
+
"#{UI.colorize(key, :blue)}: #{value_str}",
|
443
|
+
padding: [1, 2, 1, 2],
|
444
|
+
style: {border: {fg: :green}}
|
445
|
+
)
|
446
|
+
else
|
447
|
+
puts UI.box(
|
448
|
+
"Error",
|
449
|
+
"Key '#{UI.colorize(key, :yellow)}' not found in configuration",
|
450
|
+
padding: [1, 2, 1, 2],
|
451
|
+
style: {border: {fg: :red}}
|
452
|
+
)
|
453
|
+
exit 1
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
desc "set KEY=VALUE", "Set a configuration setting"
|
458
|
+
option :global, type: :boolean, aliases: "-g",
|
459
|
+
desc: "Set in global user config instead of project config"
|
460
|
+
def set(key_value)
|
461
|
+
key, value = key_value.split("=", 2)
|
462
|
+
|
463
|
+
unless value
|
464
|
+
puts "Error: Invalid format. Use KEY=VALUE"
|
465
|
+
exit 1
|
466
|
+
end
|
467
|
+
|
468
|
+
path = options[:global] ? USER_CONFIG_PATH : PROJECT_CONFIG_PATH
|
469
|
+
config = load_config(path) || {}
|
470
|
+
|
471
|
+
# Convert string values to appropriate types
|
472
|
+
value = case value.downcase
|
473
|
+
when "true" then true
|
474
|
+
when "false" then false
|
475
|
+
when /^\d+$/ then value.to_i
|
476
|
+
when /^\d+\.\d+$/ then value.to_f
|
477
|
+
else value
|
478
|
+
end
|
479
|
+
|
480
|
+
config[key] = value
|
481
|
+
save_config(path, config)
|
482
|
+
|
483
|
+
puts UI.box(
|
484
|
+
"Configuration Updated",
|
485
|
+
"Set #{UI.colorize(key, :blue)} to #{UI.colorize(value.to_s, :green)} in #{path}",
|
486
|
+
padding: [1, 2, 1, 2],
|
487
|
+
style: {border: {fg: :green}}
|
488
|
+
)
|
489
|
+
end
|
490
|
+
|
491
|
+
desc "init", "Initialize configuration"
|
492
|
+
option :global, type: :boolean, aliases: "-g",
|
493
|
+
desc: "Initialize global user config instead of project config"
|
494
|
+
def init
|
495
|
+
path = options[:global] ? USER_CONFIG_PATH : PROJECT_CONFIG_PATH
|
496
|
+
|
497
|
+
if File.exist?(path)
|
498
|
+
puts UI.box(
|
499
|
+
"Configuration Exists",
|
500
|
+
"Configuration already exists at #{UI.colorize(path, :blue)}",
|
501
|
+
padding: [1, 2, 1, 2],
|
502
|
+
style: {border: {fg: :yellow}}
|
503
|
+
)
|
504
|
+
return
|
505
|
+
end
|
506
|
+
|
507
|
+
config = {
|
508
|
+
"model" => "gpt-4o-mini",
|
509
|
+
"log_level" => "info"
|
510
|
+
# Add other default configuration options here
|
511
|
+
}
|
512
|
+
|
513
|
+
save_config(path, config)
|
514
|
+
|
515
|
+
# Format the config for display
|
516
|
+
config_str = format_config(config)
|
517
|
+
|
518
|
+
puts UI.box(
|
519
|
+
"Configuration Initialized",
|
520
|
+
"Created configuration at #{UI.colorize(path, :blue)}\n\n#{config_str}",
|
521
|
+
padding: [1, 2, 1, 2],
|
522
|
+
style: {border: {fg: :green}}
|
523
|
+
)
|
524
|
+
end
|
525
|
+
|
526
|
+
private
|
527
|
+
|
528
|
+
def load_config(path)
|
529
|
+
return unless File.exist?(path)
|
530
|
+
|
531
|
+
begin
|
532
|
+
YAML.load_file(path)
|
533
|
+
rescue => e
|
534
|
+
puts "Error loading configuration from #{path}: #{e.message}"
|
535
|
+
nil
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
def save_config(path, config)
|
540
|
+
# Create directory if it doesn't exist
|
541
|
+
dir = File.dirname(path)
|
542
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
543
|
+
|
544
|
+
File.write(path, YAML.dump(config))
|
545
|
+
end
|
546
|
+
|
547
|
+
def active_config
|
548
|
+
# Combine user and project configs, with project taking precedence
|
549
|
+
user_config = load_config(USER_CONFIG_PATH) || {}
|
550
|
+
project_config = load_config(PROJECT_CONFIG_PATH) || {}
|
551
|
+
|
552
|
+
user_config.merge(project_config)
|
553
|
+
end
|
554
|
+
|
555
|
+
def format_config(config)
|
556
|
+
if config.nil? || config.empty?
|
557
|
+
" #{UI.colorize("[empty]", :yellow)}\n"
|
558
|
+
else
|
559
|
+
config.map do |key, value|
|
560
|
+
value_str = case value
|
561
|
+
when true then UI.colorize("true", :green)
|
562
|
+
when false then UI.colorize("false", :red)
|
563
|
+
when String then "\"#{value}\""
|
564
|
+
else value.to_s
|
565
|
+
end
|
566
|
+
|
567
|
+
" #{UI.colorize(key, :blue)}: #{value_str}"
|
568
|
+
end.join("\n") + "\n"
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
desc "config", "Configure Agentic settings"
|
574
|
+
subcommand "config", ConfigCommands
|
575
|
+
|
576
|
+
desc "capabilities", "Manage capability registry"
|
577
|
+
subcommand "capabilities", Capabilities
|
578
|
+
|
579
|
+
private
|
580
|
+
|
581
|
+
# Asks the user if they want to adjust the plan
|
582
|
+
# @return [Boolean] true if user wants to adjust, false otherwise
|
583
|
+
def ask_user_for_plan_adjustment
|
584
|
+
puts UI.colorize("Would you like to adjust this plan? (y/n)", :cyan)
|
585
|
+
response = $stdin.gets&.chomp&.downcase
|
586
|
+
response == "y" || response == "yes"
|
587
|
+
end
|
588
|
+
|
589
|
+
# Determines if we should ask the user about executing the plan
|
590
|
+
# @return [Boolean] true if we should ask, false otherwise
|
591
|
+
def should_ask_for_execution?
|
592
|
+
!options[:quiet] && !options[:execute] && !options[:no_interactive]
|
593
|
+
end
|
594
|
+
|
595
|
+
# Asks the user if they want to execute the plan
|
596
|
+
# @return [Boolean] true if user wants to execute, false otherwise
|
597
|
+
def ask_user_for_execution
|
598
|
+
puts
|
599
|
+
puts UI.colorize("Would you like to execute this plan now? (y/n)", :cyan)
|
600
|
+
response = $stdin.gets&.chomp&.downcase
|
601
|
+
response == "y" || response == "yes"
|
602
|
+
end
|
603
|
+
|
604
|
+
# Executes a plan immediately (from plan command)
|
605
|
+
# @param execution_plan [ExecutionPlan] The execution plan to execute
|
606
|
+
def execute_plan_immediately(execution_plan)
|
607
|
+
# Convert ExecutionPlan to tasks
|
608
|
+
tasks = execution_plan.tasks.map do |task_def|
|
609
|
+
Task.new(
|
610
|
+
description: task_def.description,
|
611
|
+
agent_spec: task_def.agent,
|
612
|
+
input: {}
|
613
|
+
)
|
614
|
+
end
|
615
|
+
|
616
|
+
# Execute the tasks
|
617
|
+
execute_tasks(tasks)
|
618
|
+
end
|
619
|
+
|
620
|
+
# Shared execution logic for both execute command and immediate execution
|
621
|
+
# @param tasks [Array<Task>] The tasks to execute
|
622
|
+
def execute_tasks(tasks)
|
623
|
+
say UI.colorize("Executing plan...", :green) unless options[:quiet]
|
624
|
+
|
625
|
+
# Determine output format from file extension if provided
|
626
|
+
output_format = determine_output_format(options[:file])
|
627
|
+
|
628
|
+
# Create an execution observer for real-time feedback
|
629
|
+
observer = ExecutionObserver.new(options.merge(output_format: output_format, holistic_display: true))
|
630
|
+
|
631
|
+
# Setup the PlanOrchestrator with the observer's lifecycle hooks
|
632
|
+
orchestrator = PlanOrchestrator.new(
|
633
|
+
concurrency_limit: options[:max_concurrency] || 10,
|
634
|
+
lifecycle_hooks: observer.lifecycle_hooks
|
635
|
+
)
|
636
|
+
|
637
|
+
# Add tasks to the orchestrator
|
638
|
+
tasks.each do |task|
|
639
|
+
orchestrator.add_task(task)
|
640
|
+
end
|
641
|
+
|
642
|
+
# Show the total number of tasks
|
643
|
+
unless options[:quiet]
|
644
|
+
puts UI.colorize("Total tasks: #{tasks.size}", :blue)
|
645
|
+
puts
|
646
|
+
end
|
647
|
+
|
648
|
+
# Configure LLM for agent execution
|
649
|
+
llm_config = LlmConfig.new
|
650
|
+
llm_config.model = options[:model] if options[:model]
|
651
|
+
|
652
|
+
# Setup signal handler for graceful cancellation
|
653
|
+
setup_cancellation_handler(orchestrator, observer)
|
654
|
+
|
655
|
+
# Execute the plan
|
656
|
+
begin
|
657
|
+
result = orchestrator.execute_plan(DefaultAgentProvider.new(llm_config))
|
658
|
+
|
659
|
+
# Always save result to file
|
660
|
+
save_result_to_file(result, options, observer)
|
661
|
+
rescue Interrupt
|
662
|
+
# Handle Ctrl+C gracefully - signal handler will take care of cleanup
|
663
|
+
# This rescue is here just in case the signal doesn't propagate properly
|
664
|
+
puts "\n#{UI.colorize("⚠", :yellow)} Execution interrupted"
|
665
|
+
exit(130)
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
# Adjusts the plan based on user input
|
670
|
+
# @param execution_plan [ExecutionPlan] The original execution plan
|
671
|
+
# @param config [LlmConfig] The LLM configuration
|
672
|
+
# @return [ExecutionPlan] The adjusted execution plan
|
673
|
+
def adjust_plan_with_user_input(execution_plan, config)
|
674
|
+
puts UI.colorize("What adjustments would you like to make to the plan?", :cyan)
|
675
|
+
puts UI.colorize("(Describe what you'd like to add, remove, or modify)", :dark)
|
676
|
+
|
677
|
+
user_input = $stdin.gets&.chomp || ""
|
678
|
+
|
679
|
+
return execution_plan if user_input.strip.empty?
|
680
|
+
|
681
|
+
# Use LLM to adjust the plan based on user input
|
682
|
+
adjusted_plan = UI.with_spinner("Adjusting plan based on your feedback") do
|
683
|
+
adjust_plan_via_llm(execution_plan, user_input, config)
|
684
|
+
end
|
685
|
+
|
686
|
+
# Show the adjusted plan
|
687
|
+
puts UI.colorize("\nAdjusted Plan:", :green)
|
688
|
+
puts format_plan(adjusted_plan)
|
689
|
+
puts
|
690
|
+
|
691
|
+
adjusted_plan
|
692
|
+
end
|
693
|
+
|
694
|
+
# Uses LLM to adjust the plan based on user feedback
|
695
|
+
# @param execution_plan [ExecutionPlan] The original execution plan
|
696
|
+
# @param user_feedback [String] The user's feedback for adjustments
|
697
|
+
# @param config [LlmConfig] The LLM configuration
|
698
|
+
# @return [ExecutionPlan] The adjusted execution plan
|
699
|
+
def adjust_plan_via_llm(execution_plan, user_feedback, config)
|
700
|
+
system_message = "You are an expert project planner. Your task is to adjust an existing execution plan based on user feedback."
|
701
|
+
|
702
|
+
current_plan = {
|
703
|
+
tasks: execution_plan.tasks.map do |task|
|
704
|
+
{
|
705
|
+
description: task.description,
|
706
|
+
agent: {
|
707
|
+
name: task.agent.name,
|
708
|
+
description: task.agent.description,
|
709
|
+
instructions: task.agent.instructions
|
710
|
+
}
|
711
|
+
}
|
712
|
+
end,
|
713
|
+
expected_answer: {
|
714
|
+
format: execution_plan.expected_answer.format,
|
715
|
+
sections: execution_plan.expected_answer.sections,
|
716
|
+
length: execution_plan.expected_answer.length
|
717
|
+
}
|
718
|
+
}
|
719
|
+
|
720
|
+
user_message = <<~MSG
|
721
|
+
Current Plan:
|
722
|
+
#{JSON.pretty_generate(current_plan)}
|
723
|
+
|
724
|
+
User Feedback:
|
725
|
+
#{user_feedback}
|
726
|
+
|
727
|
+
Please adjust the plan based on the user's feedback. Maintain the same structure but modify tasks as requested.
|
728
|
+
MSG
|
729
|
+
|
730
|
+
schema = StructuredOutputs::Schema.new("adjusted_plan") do |s|
|
731
|
+
s.array :tasks, items: {
|
732
|
+
type: "object",
|
733
|
+
properties: {
|
734
|
+
description: {type: "string"},
|
735
|
+
agent: {
|
736
|
+
type: "object",
|
737
|
+
properties: {
|
738
|
+
name: {type: "string"},
|
739
|
+
description: {type: "string"},
|
740
|
+
instructions: {type: "string"}
|
741
|
+
},
|
742
|
+
required: %w[name description instructions]
|
743
|
+
}
|
744
|
+
},
|
745
|
+
required: %w[description agent]
|
746
|
+
}
|
747
|
+
s.object :expected_answer do |o|
|
748
|
+
o.string :format
|
749
|
+
o.array :sections, items: {type: "string"}
|
750
|
+
o.string :length
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
messages = [
|
755
|
+
{role: "system", content: system_message},
|
756
|
+
{role: "user", content: user_message}
|
757
|
+
]
|
758
|
+
|
759
|
+
llm_client = Agentic.client(config)
|
760
|
+
response = llm_client.complete(messages, output_schema: schema)
|
761
|
+
|
762
|
+
if response.successful?
|
763
|
+
tasks = response.content["tasks"].map do |task_data|
|
764
|
+
TaskDefinition.new(
|
765
|
+
description: task_data["description"],
|
766
|
+
agent: AgentSpecification.new(
|
767
|
+
name: task_data["agent"]["name"],
|
768
|
+
description: task_data["agent"]["description"],
|
769
|
+
instructions: task_data["agent"]["instructions"]
|
770
|
+
)
|
771
|
+
)
|
772
|
+
end
|
773
|
+
|
774
|
+
expected_answer = ExpectedAnswerFormat.new(
|
775
|
+
format: response.content["expected_answer"]["format"],
|
776
|
+
sections: response.content["expected_answer"]["sections"],
|
777
|
+
length: response.content["expected_answer"]["length"]
|
778
|
+
)
|
779
|
+
|
780
|
+
ExecutionPlan.new(tasks, expected_answer)
|
781
|
+
else
|
782
|
+
Agentic.logger.error("Failed to adjust plan: #{response.error&.message || response.refusal}")
|
783
|
+
execution_plan # Return original plan if adjustment fails
|
784
|
+
end
|
785
|
+
end
|
786
|
+
|
787
|
+
# Configures logging based on command options
|
788
|
+
def configure_logging
|
789
|
+
if options[:verbose]
|
790
|
+
Agentic.logger.level = :debug
|
791
|
+
Agentic.logger.formatter = proc do |severity, datetime, progname, msg|
|
792
|
+
color = case severity
|
793
|
+
when "DEBUG" then :blue
|
794
|
+
when "INFO" then :green
|
795
|
+
when "WARN" then :yellow
|
796
|
+
when "ERROR" then :red
|
797
|
+
when "FATAL" then :red
|
798
|
+
else :white
|
799
|
+
end
|
800
|
+
|
801
|
+
timestamp = UI.colorize(datetime.strftime("%Y-%m-%d %H:%M:%S"), :dark)
|
802
|
+
severity_colored = UI.colorize(severity.ljust(5), color)
|
803
|
+
|
804
|
+
"[#{timestamp}] #{severity_colored} : #{msg}\n"
|
805
|
+
end
|
806
|
+
elsif options[:quiet]
|
807
|
+
Agentic.logger.level = :warn
|
808
|
+
else
|
809
|
+
Agentic.logger.level = :info
|
810
|
+
end
|
811
|
+
end
|
812
|
+
|
813
|
+
# Checks for API token and raises error if not configured
|
814
|
+
def check_api_token!
|
815
|
+
unless Agentic.configuration.access_token
|
816
|
+
error_box = UI.box(
|
817
|
+
"Configuration Error",
|
818
|
+
"No OpenAI API token configured.\n\n" \
|
819
|
+
"You can set it using one of these methods:\n" \
|
820
|
+
"- Environment variable: OPENAI_ACCESS_TOKEN\n" \
|
821
|
+
"- Configuration file: .agentic.yml\n" \
|
822
|
+
"- Run: agentic config set api_token=your_token",
|
823
|
+
style: {border: {fg: :red}},
|
824
|
+
padding: [1, 2, 1, 2]
|
825
|
+
)
|
826
|
+
|
827
|
+
raise Thor::Error, error_box
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
# Outputs plan based on format options
|
832
|
+
def output_plan(execution_plan, options)
|
833
|
+
# Determine the save file path if --save is used
|
834
|
+
save_path = nil
|
835
|
+
output_format = options[:output]
|
836
|
+
|
837
|
+
if options[:save]
|
838
|
+
if options[:save] == "save" # Default value when --save is used without argument
|
839
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
840
|
+
# Default to JSON when saving without explicit format for better structure
|
841
|
+
if output_format == "text"
|
842
|
+
output_format = "json"
|
843
|
+
save_path = "plan-#{timestamp}.json"
|
844
|
+
else
|
845
|
+
extension = (output_format == "yaml") ? "yml" : output_format
|
846
|
+
save_path = "plan-#{timestamp}.#{extension}"
|
847
|
+
end
|
848
|
+
else
|
849
|
+
save_path = options[:save]
|
850
|
+
# When saving to a file, prefer structured format over text for better usability
|
851
|
+
if output_format == "text"
|
852
|
+
output_format = "json"
|
853
|
+
end
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
output = case output_format
|
858
|
+
when "json"
|
859
|
+
JSON.pretty_generate(execution_plan.to_h)
|
860
|
+
when "yaml"
|
861
|
+
YAML.dump(execution_plan.to_h)
|
862
|
+
else # text
|
863
|
+
format_plan(execution_plan)
|
864
|
+
end
|
865
|
+
|
866
|
+
if save_path
|
867
|
+
File.write(save_path, output)
|
868
|
+
say UI.colorize("Plan saved to #{save_path}", :green) unless options[:quiet]
|
869
|
+
else
|
870
|
+
puts output unless options[:quiet]
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
874
|
+
# Formats a plan for display
|
875
|
+
# @param execution_plan [ExecutionPlan] The execution plan
|
876
|
+
# @return [String] The formatted plan
|
877
|
+
def format_plan(execution_plan)
|
878
|
+
output = []
|
879
|
+
output << UI.colorize("═" * 80, :blue)
|
880
|
+
output << UI.colorize(" EXECUTION PLAN", :blue)
|
881
|
+
output << UI.colorize("═" * 80, :blue)
|
882
|
+
output << ""
|
883
|
+
|
884
|
+
output << UI.colorize("Tasks:", :green)
|
885
|
+
execution_plan.tasks.each_with_index do |task, index|
|
886
|
+
# Wrap long descriptions to prevent formatting issues
|
887
|
+
description = if task.description.length > 70
|
888
|
+
"#{task.description[0..67]}..."
|
889
|
+
else
|
890
|
+
task.description
|
891
|
+
end
|
892
|
+
|
893
|
+
output << " #{UI.colorize("#{index + 1}.", :blue)} #{description}"
|
894
|
+
output << " #{UI.colorize("Agent:", :dark)} #{UI.colorize(task.agent.name, :magenta)}"
|
895
|
+
output << ""
|
896
|
+
end
|
897
|
+
|
898
|
+
output << UI.colorize("Expected Answer:", :green)
|
899
|
+
output << " #{UI.colorize("Format:", :dark)} #{UI.colorize(execution_plan.expected_answer.format, :yellow)}"
|
900
|
+
|
901
|
+
# Handle long section lists safely
|
902
|
+
if execution_plan.expected_answer.sections.empty?
|
903
|
+
sections_display = UI.colorize("(none specified)", :dark)
|
904
|
+
elsif execution_plan.expected_answer.sections.length > 3
|
905
|
+
first_three = execution_plan.expected_answer.sections[0..2]
|
906
|
+
remaining = execution_plan.expected_answer.sections.length - 3
|
907
|
+
sections_display = "#{first_three.join(", ")} #{UI.colorize("(+#{remaining} more)", :dark)}"
|
908
|
+
else
|
909
|
+
sections_display = execution_plan.expected_answer.sections.join(", ")
|
910
|
+
end
|
911
|
+
|
912
|
+
output << " #{UI.colorize("Sections:", :dark)} #{sections_display}"
|
913
|
+
output << " #{UI.colorize("Length:", :dark)} #{UI.colorize(execution_plan.expected_answer.length, :yellow)}"
|
914
|
+
output << ""
|
915
|
+
output << UI.colorize("═" * 80, :blue)
|
916
|
+
|
917
|
+
output.join("\n")
|
918
|
+
end
|
919
|
+
|
920
|
+
# Loads plan data from file or stdin based on options
|
921
|
+
def load_plan_data
|
922
|
+
if options[:plan]
|
923
|
+
JSON.parse(File.read(options[:plan]))
|
924
|
+
elsif options[:from_stdin]
|
925
|
+
stdin_content = $stdin.read
|
926
|
+
$stdin.close unless $stdin.closed?
|
927
|
+
JSON.parse(stdin_content)
|
928
|
+
end
|
929
|
+
end
|
930
|
+
|
931
|
+
# Initializes task instances from plan data
|
932
|
+
def initialize_tasks(plan_data)
|
933
|
+
tasks = []
|
934
|
+
|
935
|
+
plan_data["tasks"].each do |task_data|
|
936
|
+
task = Task.new(
|
937
|
+
description: task_data["description"],
|
938
|
+
agent_spec: task_data["agent"],
|
939
|
+
input: task_data["input"] || {}
|
940
|
+
)
|
941
|
+
tasks << task
|
942
|
+
end
|
943
|
+
|
944
|
+
tasks
|
945
|
+
end
|
946
|
+
|
947
|
+
# Outputs execution result based on format options
|
948
|
+
def output_result(result, options)
|
949
|
+
output = case options[:output]
|
950
|
+
when "json"
|
951
|
+
JSON.pretty_generate(result.to_h)
|
952
|
+
when "yaml"
|
953
|
+
YAML.dump(result.to_h)
|
954
|
+
else # text
|
955
|
+
format_execution_result(result)
|
956
|
+
end
|
957
|
+
|
958
|
+
puts output unless options[:quiet]
|
959
|
+
end
|
960
|
+
|
961
|
+
# Formats execution result as text
|
962
|
+
def format_execution_result(result)
|
963
|
+
status_text = UI.status_text(result.status.to_s, result.status)
|
964
|
+
execution_time = UI.format_duration(result.execution_time)
|
965
|
+
|
966
|
+
output = "Execution Result:\n"
|
967
|
+
output += "Status: #{status_text}\n"
|
968
|
+
output += "Execution Time: #{execution_time}\n\n"
|
969
|
+
|
970
|
+
output += "Tasks:\n"
|
971
|
+
result.tasks.each do |task_id, task_data|
|
972
|
+
task_result = result.task_result(task_id)
|
973
|
+
status = task_result ? task_result.status : :pending
|
974
|
+
|
975
|
+
status_indicator = UI.task_status_indicator(status)
|
976
|
+
description = task_data[:description]
|
977
|
+
|
978
|
+
output += " #{status_indicator} #{description}\n"
|
979
|
+
end
|
980
|
+
|
981
|
+
# Create a box for the result
|
982
|
+
UI.box(
|
983
|
+
"Execution Summary",
|
984
|
+
output,
|
985
|
+
padding: [1, 2, 1, 2],
|
986
|
+
style: {border: {fg: result.successful? ? :green : :yellow}}
|
987
|
+
)
|
988
|
+
end
|
989
|
+
|
990
|
+
# Saves execution result to file if requested
|
991
|
+
def save_result_to_file(result, options, observer = nil)
|
992
|
+
# Determine the save file path - always save to a file
|
993
|
+
save_path = if options[:file]
|
994
|
+
# User specified a file path
|
995
|
+
options[:file]
|
996
|
+
else
|
997
|
+
# Default filename with timestamp
|
998
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
999
|
+
"result-#{timestamp}.json"
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
# Determine content format
|
1003
|
+
output_format = determine_output_format(save_path)
|
1004
|
+
|
1005
|
+
# Generate content based on format
|
1006
|
+
content = if output_format == :json || !observer
|
1007
|
+
# Save as JSON (default behavior)
|
1008
|
+
JSON.pretty_generate(result.to_h)
|
1009
|
+
else
|
1010
|
+
# Use observer to generate format-specific content
|
1011
|
+
observer.generate_file_content(result, output_format)
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
# Save the content
|
1015
|
+
File.write(save_path, content)
|
1016
|
+
|
1017
|
+
say UI.colorize("Execution result saved to #{save_path}", :green) unless options[:quiet]
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
# Determines output format from file extension
|
1021
|
+
# @param file_path [String, nil] The file path
|
1022
|
+
# @return [Symbol] The detected format (:json, :markdown, :html, :text)
|
1023
|
+
def determine_output_format(file_path)
|
1024
|
+
return :text unless file_path
|
1025
|
+
|
1026
|
+
extension = File.extname(file_path).downcase
|
1027
|
+
case extension
|
1028
|
+
when ".json"
|
1029
|
+
:json
|
1030
|
+
when ".md", ".markdown"
|
1031
|
+
:markdown
|
1032
|
+
when ".html", ".htm"
|
1033
|
+
:html
|
1034
|
+
when ".txt"
|
1035
|
+
:text
|
1036
|
+
when ".yaml", ".yml"
|
1037
|
+
:yaml
|
1038
|
+
else
|
1039
|
+
:text # Default fallback
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
# Sets up signal handler for graceful cancellation
|
1044
|
+
# @param orchestrator [PlanOrchestrator] The plan orchestrator to cancel
|
1045
|
+
# @param observer [ExecutionObserver] The observer to notify of cancellation
|
1046
|
+
def setup_cancellation_handler(orchestrator, observer)
|
1047
|
+
Signal.trap("INT") do
|
1048
|
+
puts "\n#{UI.colorize("⚠", :yellow)} Cancellation requested..."
|
1049
|
+
|
1050
|
+
# Notify observer of cancellation (sets flag)
|
1051
|
+
observer.handle_cancellation if observer.respond_to?(:handle_cancellation)
|
1052
|
+
|
1053
|
+
# Cancel the plan execution
|
1054
|
+
orchestrator.cancel_plan
|
1055
|
+
|
1056
|
+
# Show cancellation message
|
1057
|
+
puts UI.box(
|
1058
|
+
"Execution Cancelled",
|
1059
|
+
"Plan execution was cancelled by user request.\nPartial results may be available.",
|
1060
|
+
padding: [1, 2, 1, 2],
|
1061
|
+
style: {border: {fg: :yellow}}
|
1062
|
+
)
|
1063
|
+
|
1064
|
+
exit(130) # Standard exit code for SIGINT
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
end
|
1068
|
+
end
|