Class: Agentic::CLI::ExecutionObserver

Inherits:
Object
  • Object
show all
Defined in:
lib/agentic/cli/execution_observer.rb

Overview

Observer that provides real-time feedback during plan execution

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ ExecutionObserver

Initialize a new execution observer

Parameters:

  • options (Hash) (defaults to: {})

    CLI options



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/agentic/cli/execution_observer.rb', line 9

def initialize(options = {})
  @options = options
  @output_format = options[:output_format] || :text
  @start_time = Time.now
  @completed_tasks = 0
  @failed_tasks = 0
  @total_tasks = 0
  @task_spinners = {}
  @agent_spinners = {}
  @cancellation_requested = false

  # Holistic task display state
  @holistic_display = options.fetch(:holistic_display, false)
  @task_states = {}
  @display_lines = 0
  @table_rendered = false

  # Summary panel state
  @summary_lines = 0
  @summary_rendered = false

  # Agent display state
  @built_agents = {}
  @progress_summary_lines = 0
  @display_mutex = Mutex.new
end

Instance Method Details

#after_agent_build(task_id:, task:, agent:, build_duration:) ⇒ Object

Called after an agent is built for a task

Parameters:

  • task_id (String)

    The ID of the task

  • task (Task)

    The task that got an agent

  • agent (Agent)

    The built agent

  • build_duration (Float)

    The time taken to build the agent



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/agentic/cli/execution_observer.rb', line 82

def after_agent_build(task_id:, task:, agent:, build_duration:)
  return if @options[:quiet]

  if @holistic_display
    # Track built agents
    @built_agents[task_id] = {
      role: agent.role,
      build_duration: build_duration,
      task_description: task.description
    }

    # Update task state for holistic display
    @task_states[task_id]&.merge!({
      status: @cancellation_requested ? :canceled : :agent_ready,
      agent_duration: build_duration,
      agent_role: agent.role
    })
    update_holistic_display
  elsif @agent_spinners[task_id]
    # Handle agent spinner (fallback)
    if @cancellation_requested
      @agent_spinners[task_id].error("#{UI.colorize("⚠", :yellow)} Agent building cancelled")
    else
      @agent_spinners[task_id].success(
        "#{UI.colorize("✓", :green)} Agent built: #{agent.role} (#{UI.format_duration(build_duration)})"
      )
    end
    @agent_spinners.delete(task_id)
  end
end

#after_task_failure(task_id:, task:, failure:, duration:) ⇒ Object

Called after a task fails

Parameters:

  • task_id (String)

    The ID of the task

  • task (Task)

    The task that failed

  • failure (TaskFailure)

    The failure details

  • duration (Float)

    The duration of the task execution



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/agentic/cli/execution_observer.rb', line 176

def after_task_failure(task_id:, task:, failure:, duration:)
  return if @options[:quiet]

  @failed_tasks += 1

  if @holistic_display
    # Update task state for holistic display
    @task_states[task_id]&.merge!({
      status: @cancellation_requested ? :canceled : :failed,
      duration: duration,
      error: failure.message
    })
    update_holistic_display
  else
    # Fallback to original spinner behavior
    handle_task_spinner_failure(task_id, failure, duration)
    display_progress
  end
end

#after_task_success(task_id:, task:, result:, duration:) ⇒ Object

Called after a task is successfully executed

Parameters:

  • task_id (String)

    The ID of the task

  • task (Task)

    The task that was executed

  • result (TaskResult)

    The result of the task

  • duration (Float)

    The duration of the task execution



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/agentic/cli/execution_observer.rb', line 151

def after_task_success(task_id:, task:, result:, duration:)
  return if @options[:quiet]

  @completed_tasks += 1

  if @holistic_display
    # Update task state for holistic display
    @task_states[task_id]&.merge!({
      status: @cancellation_requested ? :canceled : :completed,
      duration: duration,
      output: result.output
    })
    update_holistic_display
  else
    # Fallback to original spinner behavior
    handle_task_spinner_success(task_id, result, duration)
    display_progress
  end
end

#before_agent_build(task_id:, task:) ⇒ Object

Called before an agent is built for a task

Parameters:

  • task_id (String)

    The ID of the task

  • task (Task)

    The task needing an agent



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/agentic/cli/execution_observer.rb', line 52

def before_agent_build(task_id:, task:)
  return if @options[:quiet]
  return if @cancellation_requested # Don't start new agents if cancellation requested

  if @holistic_display
    # Initialize task state for holistic display
    @task_states[task_id] = {
      status: :building_agent,
      description: task.description,
      start_time: Time.now,
      task: task
    }
    update_holistic_display
  else
    # Create a spinner for agent building (fallback)
    spinner = TTY::Spinner.new(
      "[:spinner] #{UI.colorize("🤖", :blue)} Building agent...",
      format: :dots
    )

    @agent_spinners[task_id] = spinner
    spinner.auto_spin
  end
end

#before_task_execution(task_id:, task:) ⇒ Object

Called before a task is executed

Parameters:

  • task_id (String)

    The ID of the task

  • task (Task)

    The task to execute



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/agentic/cli/execution_observer.rb', line 116

def before_task_execution(task_id:, task:)
  return if @options[:quiet]
  return if @cancellation_requested # Don't start new tasks if cancellation requested

  @total_tasks += 1 unless @task_spinners.key?(task_id) || @task_states.key?(task_id)

  if @holistic_display
    # Update task state for holistic display
    if @task_states[task_id]
      # Preserve existing data (like agent info) and update status
      @task_states[task_id].merge!({
        status: :in_progress,
        execution_start_time: Time.now
      })
    else
      # Create new state if it doesn't exist
      @task_states[task_id] = {
        status: :in_progress,
        description: task.description,
        start_time: Time.now,
        task: task
      }
    end
    update_holistic_display
  else
    # Fallback to original spinner behavior
    create_task_spinner(task_id, task)
  end
end

#generate_file_content(result, format) ⇒ String

Generates file content for saving based on the specified format

Parameters:

  • result (PlanExecutionResult)

    The plan execution result

  • format (Symbol)

    The target format

Returns:

  • (String)

    The formatted content



221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/agentic/cli/execution_observer.rb', line 221

def generate_file_content(result, format)
  if format == :json
    JSON.pretty_generate(result.to_h)
  else
    # Use LLM to generate format-specific content
    generate_formatted_output(
      result.results.values.select(&:successful?),
      result.tasks,
      format
    )
  end
end

#handle_cancellationObject

Handles cancellation by setting a flag (safe to call from signal context)



370
371
372
# File 'lib/agentic/cli/execution_observer.rb', line 370

def handle_cancellation
  @cancellation_requested = true
end

#lifecycle_hooksHash

Builds lifecycle hooks for the plan orchestrator

Returns:

  • (Hash)

    The lifecycle hooks



38
39
40
41
42
43
44
45
46
47
# File 'lib/agentic/cli/execution_observer.rb', line 38

def lifecycle_hooks
  {
    before_agent_build: method(:before_agent_build),
    after_agent_build: method(:after_agent_build),
    before_task_execution: method(:before_task_execution),
    after_task_success: method(:after_task_success),
    after_task_failure: method(:after_task_failure),
    plan_completed: method(:plan_completed)
  }
end

#plan_completed(plan_id:, status:, execution_time:, tasks:, results:) ⇒ Object

Called when the plan execution is completed

Parameters:

  • plan_id (String)

    The ID of the plan

  • status (Symbol)

    The status of the plan execution

  • execution_time (Float)

    The execution time in seconds

  • tasks (Hash)

    The tasks that were executed

  • results (Hash)

    The results of the task executions



202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/agentic/cli/execution_observer.rb', line 202

def plan_completed(plan_id:, status:, execution_time:, tasks:, results:)
  return if @options[:quiet]

  # Always save to file now - determine the output path
  save_path = determine_save_path(@options[:file])
  absolute_path = File.expand_path(save_path)

  # Show initial summary panel with progress
  show_initial_summary(status, execution_time, absolute_path)

  # Generate and display final preview with callback support
  preview = generate_output_preview(results, tasks, status, execution_time, absolute_path)
  show_final_summary(status, execution_time, absolute_path, preview)
end

#show_final_summary(status, execution_time, absolute_path, preview) ⇒ Object

Shows the final summary panel with complete preview

Parameters:

  • status (Symbol)

    The execution status

  • execution_time (Float)

    The execution time in seconds

  • absolute_path (String)

    The output file path

  • preview (String)

    The generated preview content



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/agentic/cli/execution_observer.rb', line 281

def show_final_summary(status, execution_time, absolute_path, preview)
  total_time = UI.format_duration(execution_time)

  result_color = case status
  when :completed
    :green
  when :partial_failure
    :yellow
  else
    :red
  end

  # Build final summary content
  summary_content = [
    "Status: #{UI.status_text(status, status)}",
    "Tasks: #{@total_tasks} total, " \
    "#{UI.colorize(@completed_tasks.to_s, :green)} completed, " \
    "#{UI.colorize(@failed_tasks.to_s, :red)} failed",
    "Time: #{total_time}",
    "",
    "Output: #{UI.colorize(absolute_path, :blue)}",
    "",
    "Preview:",
    preview
  ]

  summary = UI.box(
    "Execution Summary",
    summary_content.join("\n"),
    style: {border: {fg: result_color}}
  )

  # Clear previous summary if it was rendered
  if @summary_rendered && @summary_lines > 0
    UI.clear_and_reposition(@summary_lines)
  end

  puts "\n#{summary}" if !summary.empty?
end

#show_initial_summary(status, execution_time, absolute_path) ⇒ Object

Shows the initial summary panel with progress information

Parameters:

  • status (Symbol)

    The execution status

  • execution_time (Float)

    The execution time in seconds

  • absolute_path (String)

    The output file path



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/agentic/cli/execution_observer.rb', line 238

def show_initial_summary(status, execution_time, absolute_path)
  total_time = UI.format_duration(execution_time)

  result_color = case status
  when :completed
    :green
  when :partial_failure
    :yellow
  else
    :red
  end

  # Build initial summary content
  summary_content = [
    "Status: #{UI.status_text(status, status)}",
    "Tasks: #{@total_tasks} total, " \
    "#{UI.colorize(@completed_tasks.to_s, :green)} completed, " \
    "#{UI.colorize(@failed_tasks.to_s, :red)} failed",
    "Time: #{total_time}",
    "",
    "Output: #{UI.colorize(absolute_path, :blue)}",
    "",
    "Generating output preview..."
  ]

  summary = UI.box(
    "Execution Summary",
    summary_content.join("\n"),
    style: {border: {fg: result_color}}
  )

  puts "\n#{summary}" if !summary.empty?

  # Track summary state
  @summary_lines = summary.lines.count + 1 # +1 for the newline before
  @summary_rendered = true
end

#update_summary_with_message(status, execution_time, absolute_path, message) ⇒ Object

Updates the summary panel with a specific message

Parameters:

  • status (Symbol)

    The execution status

  • execution_time (Float)

    The execution time in seconds

  • absolute_path (String)

    The output file path

  • message (String)

    The message to display



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/agentic/cli/execution_observer.rb', line 326

def update_summary_with_message(status, execution_time, absolute_path, message)
  total_time = UI.format_duration(execution_time)

  result_color = case status
  when :completed
    :green
  when :partial_failure
    :yellow
  else
    :red
  end

  # Build summary content with the custom message
  summary_content = [
    "Status: #{UI.status_text(status, status)}",
    "Tasks: #{@total_tasks} total, " \
    "#{UI.colorize(@completed_tasks.to_s, :green)} completed, " \
    "#{UI.colorize(@failed_tasks.to_s, :red)} failed",
    "Time: #{total_time}",
    "",
    "Output: #{UI.colorize(absolute_path, :blue)}",
    "",
    message
  ]

  summary = UI.box(
    "Execution Summary",
    summary_content.join("\n"),
    style: {border: {fg: result_color}}
  )

  # Clear previous summary if it was rendered
  if @summary_rendered && @summary_lines > 0
    UI.clear_and_reposition(@summary_lines)
  end

  puts "\n#{summary}" if !summary.empty

  # Update tracking
  @summary_lines = summary.lines.count + 1 # +1 for the newline before
  @summary_rendered = true
end