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
@@ -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
|