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,796 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ class CLI < Thor
5
+ # Observer that provides real-time feedback during plan execution
6
+ class ExecutionObserver
7
+ # Initialize a new execution observer
8
+ # @param options [Hash] CLI options
9
+ def initialize(options = {})
10
+ @options = options
11
+ @output_format = options[:output_format] || :text
12
+ @start_time = Time.now
13
+ @completed_tasks = 0
14
+ @failed_tasks = 0
15
+ @total_tasks = 0
16
+ @task_spinners = {}
17
+ @agent_spinners = {}
18
+ @cancellation_requested = false
19
+
20
+ # Holistic task display state
21
+ @holistic_display = options.fetch(:holistic_display, false)
22
+ @task_states = {}
23
+ @display_lines = 0
24
+ @table_rendered = false
25
+
26
+ # Summary panel state
27
+ @summary_lines = 0
28
+ @summary_rendered = false
29
+
30
+ # Agent display state
31
+ @built_agents = {}
32
+ @progress_summary_lines = 0
33
+ @display_mutex = Mutex.new
34
+ end
35
+
36
+ # Builds lifecycle hooks for the plan orchestrator
37
+ # @return [Hash] The lifecycle hooks
38
+ def lifecycle_hooks
39
+ {
40
+ before_agent_build: method(:before_agent_build),
41
+ after_agent_build: method(:after_agent_build),
42
+ before_task_execution: method(:before_task_execution),
43
+ after_task_success: method(:after_task_success),
44
+ after_task_failure: method(:after_task_failure),
45
+ plan_completed: method(:plan_completed)
46
+ }
47
+ end
48
+
49
+ # Called before an agent is built for a task
50
+ # @param task_id [String] The ID of the task
51
+ # @param task [Task] The task needing an agent
52
+ def before_agent_build(task_id:, task:)
53
+ return if @options[:quiet]
54
+ return if @cancellation_requested # Don't start new agents if cancellation requested
55
+
56
+ if @holistic_display
57
+ # Initialize task state for holistic display
58
+ @task_states[task_id] = {
59
+ status: :building_agent,
60
+ description: task.description,
61
+ start_time: Time.now,
62
+ task: task
63
+ }
64
+ update_holistic_display
65
+ else
66
+ # Create a spinner for agent building (fallback)
67
+ spinner = TTY::Spinner.new(
68
+ "[:spinner] #{UI.colorize("🤖", :blue)} Building agent...",
69
+ format: :dots
70
+ )
71
+
72
+ @agent_spinners[task_id] = spinner
73
+ spinner.auto_spin
74
+ end
75
+ end
76
+
77
+ # Called after an agent is built for a task
78
+ # @param task_id [String] The ID of the task
79
+ # @param task [Task] The task that got an agent
80
+ # @param agent [Agent] The built agent
81
+ # @param build_duration [Float] The time taken to build the agent
82
+ def after_agent_build(task_id:, task:, agent:, build_duration:)
83
+ return if @options[:quiet]
84
+
85
+ if @holistic_display
86
+ # Track built agents
87
+ @built_agents[task_id] = {
88
+ role: agent.role,
89
+ build_duration: build_duration,
90
+ task_description: task.description
91
+ }
92
+
93
+ # Update task state for holistic display
94
+ @task_states[task_id]&.merge!({
95
+ status: @cancellation_requested ? :canceled : :agent_ready,
96
+ agent_duration: build_duration,
97
+ agent_role: agent.role
98
+ })
99
+ update_holistic_display
100
+ elsif @agent_spinners[task_id]
101
+ # Handle agent spinner (fallback)
102
+ if @cancellation_requested
103
+ @agent_spinners[task_id].error("#{UI.colorize("⚠", :yellow)} Agent building cancelled")
104
+ else
105
+ @agent_spinners[task_id].success(
106
+ "#{UI.colorize("✓", :green)} Agent built: #{agent.role} (#{UI.format_duration(build_duration)})"
107
+ )
108
+ end
109
+ @agent_spinners.delete(task_id)
110
+ end
111
+ end
112
+
113
+ # Called before a task is executed
114
+ # @param task_id [String] The ID of the task
115
+ # @param task [Task] The task to execute
116
+ def before_task_execution(task_id:, task:)
117
+ return if @options[:quiet]
118
+ return if @cancellation_requested # Don't start new tasks if cancellation requested
119
+
120
+ @total_tasks += 1 unless @task_spinners.key?(task_id) || @task_states.key?(task_id)
121
+
122
+ if @holistic_display
123
+ # Update task state for holistic display
124
+ if @task_states[task_id]
125
+ # Preserve existing data (like agent info) and update status
126
+ @task_states[task_id].merge!({
127
+ status: :in_progress,
128
+ execution_start_time: Time.now
129
+ })
130
+ else
131
+ # Create new state if it doesn't exist
132
+ @task_states[task_id] = {
133
+ status: :in_progress,
134
+ description: task.description,
135
+ start_time: Time.now,
136
+ task: task
137
+ }
138
+ end
139
+ update_holistic_display
140
+ else
141
+ # Fallback to original spinner behavior
142
+ create_task_spinner(task_id, task)
143
+ end
144
+ end
145
+
146
+ # Called after a task is successfully executed
147
+ # @param task_id [String] The ID of the task
148
+ # @param task [Task] The task that was executed
149
+ # @param result [TaskResult] The result of the task
150
+ # @param duration [Float] The duration of the task execution
151
+ def after_task_success(task_id:, task:, result:, duration:)
152
+ return if @options[:quiet]
153
+
154
+ @completed_tasks += 1
155
+
156
+ if @holistic_display
157
+ # Update task state for holistic display
158
+ @task_states[task_id]&.merge!({
159
+ status: @cancellation_requested ? :canceled : :completed,
160
+ duration: duration,
161
+ output: result.output
162
+ })
163
+ update_holistic_display
164
+ else
165
+ # Fallback to original spinner behavior
166
+ handle_task_spinner_success(task_id, result, duration)
167
+ display_progress
168
+ end
169
+ end
170
+
171
+ # Called after a task fails
172
+ # @param task_id [String] The ID of the task
173
+ # @param task [Task] The task that failed
174
+ # @param failure [TaskFailure] The failure details
175
+ # @param duration [Float] The duration of the task execution
176
+ def after_task_failure(task_id:, task:, failure:, duration:)
177
+ return if @options[:quiet]
178
+
179
+ @failed_tasks += 1
180
+
181
+ if @holistic_display
182
+ # Update task state for holistic display
183
+ @task_states[task_id]&.merge!({
184
+ status: @cancellation_requested ? :canceled : :failed,
185
+ duration: duration,
186
+ error: failure.message
187
+ })
188
+ update_holistic_display
189
+ else
190
+ # Fallback to original spinner behavior
191
+ handle_task_spinner_failure(task_id, failure, duration)
192
+ display_progress
193
+ end
194
+ end
195
+
196
+ # Called when the plan execution is completed
197
+ # @param plan_id [String] The ID of the plan
198
+ # @param status [Symbol] The status of the plan execution
199
+ # @param execution_time [Float] The execution time in seconds
200
+ # @param tasks [Hash] The tasks that were executed
201
+ # @param results [Hash] The results of the task executions
202
+ def plan_completed(plan_id:, status:, execution_time:, tasks:, results:)
203
+ return if @options[:quiet]
204
+
205
+ # Always save to file now - determine the output path
206
+ save_path = determine_save_path(@options[:file])
207
+ absolute_path = File.expand_path(save_path)
208
+
209
+ # Show initial summary panel with progress
210
+ show_initial_summary(status, execution_time, absolute_path)
211
+
212
+ # Generate and display final preview with callback support
213
+ preview = generate_output_preview(results, tasks, status, execution_time, absolute_path)
214
+ show_final_summary(status, execution_time, absolute_path, preview)
215
+ end
216
+
217
+ # Generates file content for saving based on the specified format
218
+ # @param result [PlanExecutionResult] The plan execution result
219
+ # @param format [Symbol] The target format
220
+ # @return [String] The formatted content
221
+ def generate_file_content(result, format)
222
+ if format == :json
223
+ JSON.pretty_generate(result.to_h)
224
+ else
225
+ # Use LLM to generate format-specific content
226
+ generate_formatted_output(
227
+ result.results.values.select(&:successful?),
228
+ result.tasks,
229
+ format
230
+ )
231
+ end
232
+ end
233
+
234
+ # Shows the initial summary panel with progress information
235
+ # @param status [Symbol] The execution status
236
+ # @param execution_time [Float] The execution time in seconds
237
+ # @param absolute_path [String] The output file path
238
+ def show_initial_summary(status, execution_time, absolute_path)
239
+ total_time = UI.format_duration(execution_time)
240
+
241
+ result_color = case status
242
+ when :completed
243
+ :green
244
+ when :partial_failure
245
+ :yellow
246
+ else
247
+ :red
248
+ end
249
+
250
+ # Build initial summary content
251
+ summary_content = [
252
+ "Status: #{UI.status_text(status, status)}",
253
+ "Tasks: #{@total_tasks} total, " \
254
+ "#{UI.colorize(@completed_tasks.to_s, :green)} completed, " \
255
+ "#{UI.colorize(@failed_tasks.to_s, :red)} failed",
256
+ "Time: #{total_time}",
257
+ "",
258
+ "Output: #{UI.colorize(absolute_path, :blue)}",
259
+ "",
260
+ "Generating output preview..."
261
+ ]
262
+
263
+ summary = UI.box(
264
+ "Execution Summary",
265
+ summary_content.join("\n"),
266
+ style: {border: {fg: result_color}}
267
+ )
268
+
269
+ puts "\n#{summary}" if !summary.empty?
270
+
271
+ # Track summary state
272
+ @summary_lines = summary.lines.count + 1 # +1 for the newline before
273
+ @summary_rendered = true
274
+ end
275
+
276
+ # Shows the final summary panel with complete preview
277
+ # @param status [Symbol] The execution status
278
+ # @param execution_time [Float] The execution time in seconds
279
+ # @param absolute_path [String] The output file path
280
+ # @param preview [String] The generated preview content
281
+ def show_final_summary(status, execution_time, absolute_path, preview)
282
+ total_time = UI.format_duration(execution_time)
283
+
284
+ result_color = case status
285
+ when :completed
286
+ :green
287
+ when :partial_failure
288
+ :yellow
289
+ else
290
+ :red
291
+ end
292
+
293
+ # Build final summary content
294
+ summary_content = [
295
+ "Status: #{UI.status_text(status, status)}",
296
+ "Tasks: #{@total_tasks} total, " \
297
+ "#{UI.colorize(@completed_tasks.to_s, :green)} completed, " \
298
+ "#{UI.colorize(@failed_tasks.to_s, :red)} failed",
299
+ "Time: #{total_time}",
300
+ "",
301
+ "Output: #{UI.colorize(absolute_path, :blue)}",
302
+ "",
303
+ "Preview:",
304
+ preview
305
+ ]
306
+
307
+ summary = UI.box(
308
+ "Execution Summary",
309
+ summary_content.join("\n"),
310
+ style: {border: {fg: result_color}}
311
+ )
312
+
313
+ # Clear previous summary if it was rendered
314
+ if @summary_rendered && @summary_lines > 0
315
+ UI.clear_and_reposition(@summary_lines)
316
+ end
317
+
318
+ puts "\n#{summary}" if !summary.empty?
319
+ end
320
+
321
+ # Updates the summary panel with a specific message
322
+ # @param status [Symbol] The execution status
323
+ # @param execution_time [Float] The execution time in seconds
324
+ # @param absolute_path [String] The output file path
325
+ # @param message [String] The message to display
326
+ def update_summary_with_message(status, execution_time, absolute_path, message)
327
+ total_time = UI.format_duration(execution_time)
328
+
329
+ result_color = case status
330
+ when :completed
331
+ :green
332
+ when :partial_failure
333
+ :yellow
334
+ else
335
+ :red
336
+ end
337
+
338
+ # Build summary content with the custom message
339
+ summary_content = [
340
+ "Status: #{UI.status_text(status, status)}",
341
+ "Tasks: #{@total_tasks} total, " \
342
+ "#{UI.colorize(@completed_tasks.to_s, :green)} completed, " \
343
+ "#{UI.colorize(@failed_tasks.to_s, :red)} failed",
344
+ "Time: #{total_time}",
345
+ "",
346
+ "Output: #{UI.colorize(absolute_path, :blue)}",
347
+ "",
348
+ message
349
+ ]
350
+
351
+ summary = UI.box(
352
+ "Execution Summary",
353
+ summary_content.join("\n"),
354
+ style: {border: {fg: result_color}}
355
+ )
356
+
357
+ # Clear previous summary if it was rendered
358
+ if @summary_rendered && @summary_lines > 0
359
+ UI.clear_and_reposition(@summary_lines)
360
+ end
361
+
362
+ puts "\n#{summary}" if !summary.empty
363
+
364
+ # Update tracking
365
+ @summary_lines = summary.lines.count + 1 # +1 for the newline before
366
+ @summary_rendered = true
367
+ end
368
+
369
+ # Handles cancellation by setting a flag (safe to call from signal context)
370
+ def handle_cancellation
371
+ @cancellation_requested = true
372
+ end
373
+
374
+ private
375
+
376
+ # Determines the save path for output file
377
+ # @param file_option [String] The file option from CLI
378
+ # @return [String] The resolved file path
379
+ def determine_save_path(file_option)
380
+ if file_option
381
+ # User specified a file path
382
+ file_option
383
+ else
384
+ # Default filename with timestamp
385
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
386
+ "result-#{timestamp}.json"
387
+ end
388
+ end
389
+
390
+ # Generates a preview of the output (first 2-3 lines)
391
+ # @param results [Hash] The task results
392
+ # @param tasks [Hash] The task data
393
+ # @param status [Symbol] The execution status for summary panel updates
394
+ # @param execution_time [Float] The execution time for summary panel updates
395
+ # @param absolute_path [String] The output file path for summary panel updates
396
+ # @return [String] Preview text
397
+ def generate_output_preview(results, tasks, status = nil, execution_time = nil, absolute_path = nil)
398
+ # Create callback to update summary panel if we have the required parameters
399
+ update_callback = if status && execution_time && absolute_path
400
+ proc { |message| update_summary_with_message(status, execution_time, absolute_path, message) }
401
+ end
402
+
403
+ consolidated = format_consolidated_output(results, tasks, update_callback)
404
+
405
+ # Split into lines and take first 3 lines
406
+ lines = consolidated.lines
407
+ preview_lines = lines.first(3)
408
+
409
+ # Add ellipsis if there are more lines
410
+ if lines.length > 3
411
+ preview_lines << "..."
412
+ end
413
+
414
+ # Join and ensure proper indentation for the box
415
+ preview_lines.map { |line| " #{line.chomp}" }.join("\n")
416
+ end
417
+
418
+ # Updates the holistic task display table
419
+ def update_holistic_display
420
+ return if @options[:quiet] || @task_states.empty?
421
+
422
+ # Use mutex to prevent concurrent display updates
423
+ @display_mutex.synchronize do
424
+ update_display_synchronized
425
+ end
426
+ end
427
+
428
+ # Synchronized display update using standard table rendering
429
+ def update_display_synchronized
430
+ # Clear previous display if it was rendered
431
+ if @table_rendered && @display_lines > 0
432
+ UI.clear_and_reposition(@display_lines)
433
+ end
434
+
435
+ # Format task data for display
436
+ tasks_for_display = @task_states.map do |task_id, task_data|
437
+ # Get agent info if available
438
+ agent_info = @built_agents[task_id]
439
+
440
+ # Merge task data with agent info for display
441
+ display_task = task_data.dup
442
+ if agent_info
443
+ display_task[:agent_role] = agent_info[:role]
444
+ display_task[:agent_duration] = agent_info[:build_duration]
445
+ end
446
+
447
+ display_task
448
+ end
449
+
450
+ # Create table display
451
+ table_output = UI.task_display_table(tasks_for_display, show_agent_column: true)
452
+ puts table_output if !table_output.empty?
453
+
454
+ # Display progress summary
455
+ display_progress_summary
456
+
457
+ # Track display state
458
+ @display_lines = table_output.lines.count + @progress_summary_lines
459
+ @table_rendered = true
460
+ end
461
+
462
+ # Displays progress summary below the table
463
+ def display_progress_summary
464
+ if @total_tasks > 0
465
+ total = @completed_tasks + @failed_tasks
466
+ if total > 0
467
+ elapsed = Time.now - @start_time
468
+ progress = (total / @total_tasks.to_f * 100).round
469
+ summary = "Progress: #{progress}% (#{total}/#{@total_tasks}) - " \
470
+ "Elapsed: #{UI.format_duration(elapsed)}"
471
+
472
+ puts UI.colorize(summary, :blue) if !summary.empty?
473
+ @progress_summary_lines = 1
474
+ else
475
+ @progress_summary_lines = 0
476
+ end
477
+ else
478
+ @progress_summary_lines = 0
479
+ end
480
+ end
481
+
482
+ # Displays a summary box of built agents
483
+ # @return [String] The formatted agent summary box
484
+ def display_agent_summary_box
485
+ return "" if @built_agents.empty?
486
+
487
+ # Create agent summary content
488
+ agent_lines = @built_agents.map do |task_id, agent_info|
489
+ duration_text = UI.format_duration(agent_info[:build_duration])
490
+ "#{UI.colorize("🤖", :blue)} #{agent_info[:role]} (#{duration_text}) → #{agent_info[:task_description]}"
491
+ end
492
+
493
+ # Add header
494
+ summary_content = [
495
+ UI.colorize("Agents Built:", :green),
496
+ "",
497
+ *agent_lines
498
+ ]
499
+
500
+ UI.box(
501
+ "Agent Summary",
502
+ summary_content.join("\n"),
503
+ style: {border: {fg: :blue}}
504
+ )
505
+ end
506
+
507
+ # Creates a task spinner (fallback for non-holistic display)
508
+ def create_task_spinner(task_id, task)
509
+ # Truncate very long descriptions to prevent UI issues
510
+ max_length = 80
511
+ display_description = if task.description.length > max_length
512
+ "#{task.description[0..max_length - 4]}..."
513
+ else
514
+ task.description
515
+ end
516
+
517
+ # Create a spinner for the task execution
518
+ spinner = TTY::Spinner.new(
519
+ "[:spinner] #{UI.colorize("▶", :blue)} #{display_description}",
520
+ format: :dots
521
+ )
522
+
523
+ @task_spinners[task_id] = {
524
+ spinner: spinner,
525
+ task: task,
526
+ start_time: Time.now
527
+ }
528
+
529
+ spinner.auto_spin
530
+ end
531
+
532
+ # Handles task spinner success (fallback for non-holistic display)
533
+ def handle_task_spinner_success(task_id, result, duration)
534
+ if @task_spinners[task_id]
535
+ spinner = @task_spinners[task_id][:spinner]
536
+
537
+ if @cancellation_requested
538
+ spinner.error("#{UI.colorize("⚠", :yellow)} Cancelled")
539
+ else
540
+ # Display task output if available and not too long
541
+ output_preview = ""
542
+ if result.output && !result.output.to_s.empty?
543
+ output_text = result.output.to_s.strip
544
+ output_preview = if output_text.length > 100
545
+ " → #{output_text[0..97]}..."
546
+ else
547
+ " → #{output_text}"
548
+ end
549
+ end
550
+
551
+ task_info = @task_spinners[task_id][:task]
552
+ task_description = task_info&.description || "Task"
553
+
554
+ spinner.success(
555
+ "#{UI.colorize("✓", :green)} #{task_description} completed#{output_preview} " \
556
+ "(#{UI.format_duration(duration)})"
557
+ )
558
+ end
559
+ end
560
+ end
561
+
562
+ # Handles task spinner failure (fallback for non-holistic display)
563
+ def handle_task_spinner_failure(task_id, failure, duration)
564
+ if @task_spinners[task_id]
565
+ spinner = @task_spinners[task_id][:spinner]
566
+ if @cancellation_requested
567
+ spinner.error("#{UI.colorize("⚠", :yellow)} Cancelled")
568
+ else
569
+ task_info = @task_spinners[task_id][:task]
570
+ task_description = task_info&.description || "Task"
571
+
572
+ spinner.error(
573
+ "#{UI.colorize("✗", :red)} #{task_description} failed - " \
574
+ "#{failure.message} (#{UI.format_duration(duration)})"
575
+ )
576
+ end
577
+ end
578
+ end
579
+
580
+ # Displays progress information
581
+ def display_progress
582
+ return if @options[:quiet]
583
+
584
+ total = @completed_tasks + @failed_tasks
585
+ elapsed = Time.now - @start_time
586
+
587
+ if @total_tasks > 0
588
+ progress = (total / @total_tasks.to_f * 100).round
589
+ if total > 0 && total < @total_tasks
590
+ # Use carriage return to overwrite the previous progress line
591
+ print "\r#{UI.colorize(
592
+ "Progress: #{progress}% (#{total}/#{@total_tasks}) - " \
593
+ "Elapsed: #{UI.format_duration(elapsed)}",
594
+ :blue
595
+ )}"
596
+ $stdout.flush
597
+ end
598
+ end
599
+ end
600
+
601
+ # Formats consolidated output from all task results
602
+ # @param results [Hash] Hash of task_id => TaskExecutionResult
603
+ # @param tasks [Hash] Hash of task_id => Task data
604
+ # @param update_callback [Proc, nil] Optional callback to update summary panel
605
+ # @return [String] Formatted output
606
+ def format_consolidated_output(results, tasks, update_callback = nil)
607
+ # Convert hash values to array and filter successful results
608
+ result_objects = results.values
609
+ successful_results = result_objects.select(&:successful?)
610
+
611
+ if successful_results.empty?
612
+ UI.colorize("No successful task outputs", :yellow)
613
+ elsif @output_format == :text
614
+ # Simple text format (existing behavior)
615
+ outputs = successful_results.map.with_index do |result, index|
616
+ output = result.output.to_s.strip
617
+ if output.empty?
618
+ "#{index + 1}. (no output)"
619
+ else
620
+ "#{index + 1}. #{output}"
621
+ end
622
+ end
623
+ outputs.join("\n")
624
+ else
625
+ # Use LLM to generate format-specific output
626
+ generate_formatted_output(successful_results, tasks, @output_format, update_callback)
627
+ end
628
+ end
629
+
630
+ # Generates format-specific output using LLM
631
+ # @param successful_results [Array<TaskExecutionResult>] The successful task results
632
+ # @param tasks [Hash] Hash of task_id => Task data
633
+ # @param format [Symbol] The target format (:markdown, :html, :json, :yaml)
634
+ # @param update_summary_callback [Proc, nil] Optional callback to update summary panel
635
+ # @return [String] Formatted output
636
+ def generate_formatted_output(successful_results, tasks, format, update_summary_callback = nil)
637
+ return simple_format_fallback(successful_results) if successful_results.empty?
638
+
639
+ # Prepare task data for LLM
640
+ task_summaries = successful_results.map.with_index do |result, index|
641
+ task_id = result.respond_to?(:task_id) ? result.task_id : "task_#{index + 1}"
642
+ task_info = tasks[task_id] || {}
643
+
644
+ {
645
+ index: index + 1,
646
+ description: task_info[:description] || "Task #{index + 1}",
647
+ output: result.output.to_s.strip
648
+ }
649
+ end
650
+
651
+ # Generate format-specific prompt
652
+ prompt = build_formatting_prompt(task_summaries, format)
653
+ format_name = format.to_s.capitalize
654
+
655
+ # Use LLM to generate formatted output
656
+ begin
657
+ llm_config = Agentic::LlmConfig.new
658
+ llm_client = Agentic::LlmClient.new(llm_config)
659
+
660
+ # Update summary panel to show generation in progress
661
+ update_summary_callback&.call("Generating #{format_name} summary...")
662
+
663
+ response = llm_client.complete([
664
+ {role: "user", content: prompt}
665
+ ])
666
+
667
+ if response.successful?
668
+ response.content
669
+ else
670
+ simple_format_fallback(successful_results)
671
+ end
672
+ rescue => e
673
+ Agentic.logger.warn("Failed to generate formatted output: #{e.message}")
674
+ simple_format_fallback(successful_results)
675
+ end
676
+ end
677
+
678
+ # Builds a prompt for LLM to format the output
679
+ # @param task_summaries [Array<Hash>] Array of task summary data
680
+ # @param format [Symbol] The target format
681
+ # @return [String] The formatting prompt
682
+ def build_formatting_prompt(task_summaries, format)
683
+ case format
684
+ when :markdown
685
+ build_markdown_prompt(task_summaries)
686
+ when :html
687
+ build_html_prompt(task_summaries)
688
+ when :json
689
+ build_json_prompt(task_summaries)
690
+ when :yaml
691
+ build_yaml_prompt(task_summaries)
692
+ else
693
+ build_generic_prompt(task_summaries, format)
694
+ end
695
+ end
696
+
697
+ # Builds markdown formatting prompt
698
+ def build_markdown_prompt(task_summaries)
699
+ task_data = task_summaries.map do |task|
700
+ "#{task[:index]}. **#{task[:description]}**\n Result: #{task[:output]}"
701
+ end.join("\n\n")
702
+
703
+ <<~PROMPT
704
+ Please format the following task execution results as a clean, professional Markdown document:
705
+
706
+ #{task_data}
707
+
708
+ Requirements:
709
+ - Use appropriate headings and structure
710
+ - Make it readable and well-organized
711
+ - Include a summary section
712
+ - Use proper Markdown formatting
713
+ - Keep the content concise but informative
714
+ PROMPT
715
+ end
716
+
717
+ # Builds HTML formatting prompt
718
+ def build_html_prompt(task_summaries)
719
+ task_data = task_summaries.map do |task|
720
+ "#{task[:index]}. #{task[:description]} → #{task[:output]}"
721
+ end.join("\n")
722
+
723
+ <<~PROMPT
724
+ Please format the following task execution results as clean HTML:
725
+
726
+ #{task_data}
727
+
728
+ Requirements:
729
+ - Use semantic HTML elements
730
+ - Include basic styling for readability
731
+ - Structure with headings and lists
732
+ - Make it professional and clean
733
+ - Include a summary section
734
+ PROMPT
735
+ end
736
+
737
+ # Builds JSON formatting prompt
738
+ def build_json_prompt(task_summaries)
739
+ <<~PROMPT
740
+ Please format the following task execution results as a well-structured JSON document:
741
+
742
+ #{task_summaries.map { |t| "#{t[:index]}. #{t[:description]} → #{t[:output]}" }.join("\n")}
743
+
744
+ Requirements:
745
+ - Create a structured JSON with summary, tasks array, and metadata
746
+ - Include task descriptions, outputs, and indices
747
+ - Make it easy to parse programmatically
748
+ - Add relevant metadata like timestamp, total tasks, etc.
749
+ PROMPT
750
+ end
751
+
752
+ # Builds YAML formatting prompt
753
+ def build_yaml_prompt(task_summaries)
754
+ <<~PROMPT
755
+ Please format the following task execution results as clean YAML:
756
+
757
+ #{task_summaries.map { |t| "#{t[:index]}. #{t[:description]} → #{t[:output]}" }.join("\n")}
758
+
759
+ Requirements:
760
+ - Use proper YAML structure with summary and tasks
761
+ - Include task descriptions and outputs
762
+ - Make it readable and well-organized
763
+ - Add metadata section
764
+ PROMPT
765
+ end
766
+
767
+ # Builds generic formatting prompt
768
+ def build_generic_prompt(task_summaries, format)
769
+ task_data = task_summaries.map do |task|
770
+ "#{task[:index]}. #{task[:description]} → #{task[:output]}"
771
+ end.join("\n")
772
+
773
+ <<~PROMPT
774
+ Please format the following task execution results in #{format} format:
775
+
776
+ #{task_data}
777
+
778
+ Make it well-structured, professional, and appropriate for the #{format} format.
779
+ PROMPT
780
+ end
781
+
782
+ # Simple fallback formatting when LLM is unavailable
783
+ def simple_format_fallback(successful_results)
784
+ outputs = successful_results.map.with_index do |result, index|
785
+ output = result.output.to_s.strip
786
+ if output.empty?
787
+ "#{index + 1}. (no output)"
788
+ else
789
+ "#{index + 1}. #{output}"
790
+ end
791
+ end
792
+ outputs.join("\n")
793
+ end
794
+ end
795
+ end
796
+ end