Skip to content

Add handle_exception method to log errors during GenerationJob execution#310

Open
TheRealNeil wants to merge 1 commit intoactiveagents:mainfrom
qlarity:fix-missing-class-method-handle_exception
Open

Add handle_exception method to log errors during GenerationJob execution#310
TheRealNeil wants to merge 1 commit intoactiveagents:mainfrom
qlarity:fix-missing-class-method-handle_exception

Conversation

@TheRealNeil
Copy link
Contributor

@TheRealNeil TheRealNeil commented Feb 8, 2026

Add missing handle_exception class method to Rescue concern

Fixes #306

Summary

  • Add handle_exception class method to ActiveAgent::Concerns::Rescue so that GenerationJob#handle_exception_with_agent_class has something to call

Root cause

GenerationJob#handle_exception_with_agent_class calls klass.handle_exception(exception) (a class method), but the Rescue concern only defines instance methods. This causes a NoMethodError when a job-level exception is raised during generate_later.

Fix

Add a handle_exception class method to the Rescue concern's class_methods block. It logs the exception class, message, and first 10 lines of the backtrace via Rails.logger.error.

Test plan

  • Trigger an unhandled exception during generate_later and confirm it is logged rather than raising NoMethodError
  • Confirm rescue_from instance-level handlers still work as expected

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a missing class-level exception handler to the ActiveAgent::Rescue concern so ActiveAgent::GenerationJob#handle_exception_with_agent_class can invoke klass.handle_exception(exception) without raising NoMethodError during generate_later.

Changes:

  • Add ActiveAgent::Rescue.handle_exception(exception) as a class method.
  • Log exception class/message and the first 10 backtrace lines when called as a job-level fallback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +27
Rails.logger.error "[#{name}] #{exception.class}: #{exception.message}"
Rails.logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
end

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle_exception logs via Rails.logger, which bypasses the framework’s configurable logger (ActiveAgent::Base.logger / config.logger) and can also raise NameError if ActiveAgent is used without Rails loaded. Prefer logging through the agent/logger accessor (with a safe fallback) so custom logger configuration is respected.

Suggested change
Rails.logger.error "[#{name}] #{exception.class}: #{exception.message}"
Rails.logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
end
agent_logger.error "[#{name}] #{exception.class}: #{exception.message}"
agent_logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
end
private
# Returns the logger to use for class-level exception handling.
#
# Prefers the including class's logger, then ActiveAgent::Base.logger,
# then Rails.logger if available, and finally falls back to a standard
# Ruby Logger to $stderr.
#
# @return [#error] a logger-like object responding to `#error`
def agent_logger
if respond_to?(:logger) && (current_logger = logger)
current_logger
elsif defined?(ActiveAgent::Base) &&
ActiveAgent::Base.respond_to?(:logger) &&
(base_logger = ActiveAgent::Base.logger)
base_logger
elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
Rails.logger
else
require "logger"
@__active_agent_rescue_logger ||= Logger.new($stderr)
end
end

Copilot uses AI. Check for mistakes.
# @return [void]
def handle_exception(exception)
Rails.logger.error "[#{name}] #{exception.class}: #{exception.message}"
Rails.logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this default handle_exception implementation will swallow the original exception (ActiveJob rescue_from considers it handled), which can cause GenerationJob failures to be silently marked successful and prevent job-level retries/discard logic from running. Consider re-raising after logging (or making the default behavior opt-in) so failures still surface to the job framework.

Suggested change
Rails.logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
Rails.logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
raise exception

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +26
# Handles exceptions raised during GenerationJob execution.
#
# Called by GenerationJob#handle_exception_with_agent_class as a
# class-level fallback when no instance is available to handle the error.
#
# @param exception [Exception] the exception to handle
# @return [void]
def handle_exception(exception)
Rails.logger.error "[#{name}] #{exception.class}: #{exception.message}"
Rails.logger.error exception.backtrace&.first(10)&.join("\n") if exception.backtrace
end
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces new behavior for background-job exception handling, but there doesn’t appear to be coverage asserting that GenerationJob calls the class-level handler and that the exception is logged/propagated as intended. Please add a test around GenerationJob#handle_exception_with_agent_class (and the default handle_exception) to prevent regressions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GenerationJob calls missing class method handle_exception

1 participant