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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.agentic.yml +2 -0
  3. data/.architecture/decisions/ArchitecturalFeatureBuilder.md +136 -0
  4. data/.architecture/decisions/ArchitectureConsiderations.md +200 -0
  5. data/.architecture/decisions/adr_001_observer_pattern_implementation.md +196 -0
  6. data/.architecture/decisions/adr_002_plan_orchestrator.md +320 -0
  7. data/.architecture/decisions/adr_003_plan_orchestrator_interface.md +179 -0
  8. data/.architecture/decisions/adrs/ADR-001-dependency-management.md +147 -0
  9. data/.architecture/decisions/adrs/ADR-002-system-boundaries.md +162 -0
  10. data/.architecture/decisions/adrs/ADR-003-content-safety.md +158 -0
  11. data/.architecture/decisions/adrs/ADR-004-agent-permissions.md +161 -0
  12. data/.architecture/decisions/adrs/ADR-005-adaptation-engine.md +127 -0
  13. data/.architecture/decisions/adrs/ADR-006-extension-system.md +273 -0
  14. data/.architecture/decisions/adrs/ADR-007-learning-system.md +156 -0
  15. data/.architecture/decisions/adrs/ADR-008-prompt-generation.md +325 -0
  16. data/.architecture/decisions/adrs/ADR-009-task-failure-handling.md +353 -0
  17. data/.architecture/decisions/adrs/ADR-010-task-input-handling.md +251 -0
  18. data/.architecture/decisions/adrs/ADR-011-task-observable-pattern.md +391 -0
  19. data/.architecture/decisions/adrs/ADR-012-task-output-handling.md +205 -0
  20. data/.architecture/decisions/adrs/ADR-013-architecture-alignment.md +211 -0
  21. data/.architecture/decisions/adrs/ADR-014-agent-capability-registry.md +80 -0
  22. data/.architecture/decisions/adrs/ADR-015-persistent-agent-store.md +100 -0
  23. data/.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md +117 -0
  24. data/.architecture/decisions/adrs/ADR-017-streaming-observability.md +171 -0
  25. data/.architecture/decisions/capability_tools_distinction.md +150 -0
  26. data/.architecture/decisions/cli_command_structure.md +61 -0
  27. data/.architecture/implementation/agent_self_assembly_implementation.md +267 -0
  28. data/.architecture/implementation/agent_self_assembly_summary.md +138 -0
  29. data/.architecture/members.yml +187 -0
  30. data/.architecture/planning/self_implementation_exercise.md +295 -0
  31. data/.architecture/planning/session_compaction_rule.md +43 -0
  32. data/.architecture/planning/streaming_observability_feature.md +223 -0
  33. data/.architecture/principles.md +151 -0
  34. data/.architecture/recalibration/0-2-0.md +92 -0
  35. data/.architecture/recalibration/agent_self_assembly.md +238 -0
  36. data/.architecture/recalibration/cli_command_structure.md +91 -0
  37. data/.architecture/recalibration/implementation_roadmap_0-2-0.md +301 -0
  38. data/.architecture/recalibration/progress_tracking_0-2-0.md +114 -0
  39. data/.architecture/recalibration_process.md +127 -0
  40. data/.architecture/reviews/0-2-0.md +181 -0
  41. data/.architecture/reviews/cli_command_duplication.md +98 -0
  42. data/.architecture/templates/adr.md +105 -0
  43. data/.architecture/templates/implementation_roadmap.md +125 -0
  44. data/.architecture/templates/progress_tracking.md +89 -0
  45. data/.architecture/templates/recalibration_plan.md +70 -0
  46. data/.architecture/templates/version_comparison.md +124 -0
  47. data/.claude/settings.local.json +13 -0
  48. data/.claude-sessions/001-task-class-architecture-implementation.md +129 -0
  49. data/.claude-sessions/002-plan-orchestrator-interface-review.md +105 -0
  50. data/.claude-sessions/architecture-governance-implementation.md +37 -0
  51. data/.claude-sessions/architecture-review-session.md +27 -0
  52. data/ArchitecturalFeatureBuilder.md +136 -0
  53. data/ArchitectureConsiderations.md +229 -0
  54. data/CHANGELOG.md +57 -2
  55. data/CLAUDE.md +111 -0
  56. data/CONTRIBUTING.md +286 -0
  57. data/MAINTAINING.md +301 -0
  58. data/README.md +582 -28
  59. data/docs/agent_capabilities_api.md +259 -0
  60. data/docs/artifact_extension_points.md +757 -0
  61. data/docs/artifact_generation_architecture.md +323 -0
  62. data/docs/artifact_implementation_plan.md +596 -0
  63. data/docs/artifact_integration_points.md +345 -0
  64. data/docs/artifact_verification_strategies.md +581 -0
  65. data/docs/streaming_observability_architecture.md +510 -0
  66. data/exe/agentic +6 -1
  67. data/lefthook.yml +5 -0
  68. data/lib/agentic/adaptation_engine.rb +124 -0
  69. data/lib/agentic/agent.rb +181 -4
  70. data/lib/agentic/agent_assembly_engine.rb +442 -0
  71. data/lib/agentic/agent_capability_registry.rb +260 -0
  72. data/lib/agentic/agent_config.rb +63 -0
  73. data/lib/agentic/agent_specification.rb +46 -0
  74. data/lib/agentic/capabilities/examples.rb +530 -0
  75. data/lib/agentic/capabilities.rb +14 -0
  76. data/lib/agentic/capability_provider.rb +146 -0
  77. data/lib/agentic/capability_specification.rb +118 -0
  78. data/lib/agentic/cli/agent.rb +31 -0
  79. data/lib/agentic/cli/capabilities.rb +191 -0
  80. data/lib/agentic/cli/config.rb +134 -0
  81. data/lib/agentic/cli/execution_observer.rb +796 -0
  82. data/lib/agentic/cli.rb +1068 -0
  83. data/lib/agentic/default_agent_provider.rb +35 -0
  84. data/lib/agentic/errors/llm_error.rb +184 -0
  85. data/lib/agentic/execution_plan.rb +53 -0
  86. data/lib/agentic/execution_result.rb +91 -0
  87. data/lib/agentic/expected_answer_format.rb +46 -0
  88. data/lib/agentic/extension/domain_adapter.rb +109 -0
  89. data/lib/agentic/extension/plugin_manager.rb +163 -0
  90. data/lib/agentic/extension/protocol_handler.rb +116 -0
  91. data/lib/agentic/extension.rb +45 -0
  92. data/lib/agentic/factory_methods.rb +9 -1
  93. data/lib/agentic/generation_stats.rb +61 -0
  94. data/lib/agentic/learning/README.md +84 -0
  95. data/lib/agentic/learning/capability_optimizer.rb +613 -0
  96. data/lib/agentic/learning/execution_history_store.rb +251 -0
  97. data/lib/agentic/learning/pattern_recognizer.rb +500 -0
  98. data/lib/agentic/learning/strategy_optimizer.rb +706 -0
  99. data/lib/agentic/learning.rb +131 -0
  100. data/lib/agentic/llm_assisted_composition_strategy.rb +188 -0
  101. data/lib/agentic/llm_client.rb +215 -15
  102. data/lib/agentic/llm_config.rb +65 -1
  103. data/lib/agentic/llm_response.rb +163 -0
  104. data/lib/agentic/logger.rb +1 -1
  105. data/lib/agentic/observable.rb +51 -0
  106. data/lib/agentic/persistent_agent_store.rb +385 -0
  107. data/lib/agentic/plan_execution_result.rb +129 -0
  108. data/lib/agentic/plan_orchestrator.rb +464 -0
  109. data/lib/agentic/plan_orchestrator_config.rb +57 -0
  110. data/lib/agentic/retry_config.rb +63 -0
  111. data/lib/agentic/retry_handler.rb +125 -0
  112. data/lib/agentic/structured_outputs.rb +1 -1
  113. data/lib/agentic/task.rb +193 -0
  114. data/lib/agentic/task_definition.rb +39 -0
  115. data/lib/agentic/task_execution_result.rb +92 -0
  116. data/lib/agentic/task_failure.rb +66 -0
  117. data/lib/agentic/task_output_schemas.rb +112 -0
  118. data/lib/agentic/task_planner.rb +54 -19
  119. data/lib/agentic/task_result.rb +48 -0
  120. data/lib/agentic/ui.rb +244 -0
  121. data/lib/agentic/verification/critic_framework.rb +116 -0
  122. data/lib/agentic/verification/llm_verification_strategy.rb +60 -0
  123. data/lib/agentic/verification/schema_verification_strategy.rb +47 -0
  124. data/lib/agentic/verification/verification_hub.rb +62 -0
  125. data/lib/agentic/verification/verification_result.rb +50 -0
  126. data/lib/agentic/verification/verification_strategy.rb +26 -0
  127. data/lib/agentic/version.rb +1 -1
  128. data/lib/agentic.rb +74 -2
  129. data/plugins/README.md +41 -0
  130. metadata +245 -6
@@ -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