Langertha

 view release on metacpan or  search on metacpan

lib/Langertha/Raider.pm  view on Meta::CPAN

package Langertha::Raider;
# ABSTRACT: Autonomous agent with conversation history and MCP tools
our $VERSION = '0.502';
use Moose;
use Future::AsyncAwait;
use Time::HiRes qw( gettimeofday tv_interval );
use Carp qw( croak );
use Module::Runtime qw( use_module );
use Scalar::Util qw( blessed );
use Langertha::Raider::Result;
use Langertha::RunContext;

with 'Langertha::Role::PluginHost', 'Langertha::Role::Runnable';


has engine => (
  is => 'ro',
  required => 1,
);


has mission => (
  is => 'ro',
  isa => 'Str',
  predicate => 'has_mission',
);


has history => (
  is => 'rw',
  isa => 'ArrayRef',
  default => sub { [] },
);


has max_iterations => (
  is => 'ro',
  isa => 'Int',
  default => 10,
);


has max_context_tokens => (
  is => 'ro',
  isa => 'Int',
  predicate => 'has_max_context_tokens',
);


has context_compress_threshold => (
  is => 'ro',
  isa => 'Num',
  default => 0.75,
);


has compression_prompt => (
  is => 'ro',
  isa => 'Str',
  lazy => 1,
  default => sub {
    'You are a conversation summarizer. Summarize the following conversation '
    . 'between a user and an AI assistant. Preserve all key facts, decisions, '
    . 'action items, file names, code references, and important context. '
    . 'Be concise but complete. The summary will replace the conversation '
    . 'history, so the assistant must be able to continue naturally.'
  },
);


has compression_engine => (
  is => 'ro',
  predicate => 'has_compression_engine',
);


has session_history => (
  is => 'ro',
  isa => 'ArrayRef',
  default => sub { [] },
);


has _last_prompt_tokens => (
  is => 'rw',
  isa => 'Int',
  predicate => 'has_last_prompt_tokens',
);

has _injections => (
  is => 'ro',
  isa => 'ArrayRef',
  default => sub { [] },
);

has on_iteration => (
  is => 'rw',
  isa => 'CodeRef',
  predicate => 'has_on_iteration',
);


has metrics => (
  is => 'rw',
  isa => 'HashRef',
  default => sub { {
    raids => 0, iterations => 0, tool_calls => 0, time_ms => 0,
  } },
);


has langfuse_trace_name => (
  is => 'ro',
  isa => 'Str',
  default => 'raid',
);


has langfuse_user_id => (
  is => 'ro',
  isa => 'Str',
  predicate => 'has_langfuse_user_id',
);


has langfuse_session_id => (
  is => 'ro',
  isa => 'Str',
  predicate => 'has_langfuse_session_id',
);


has langfuse_tags => (
  is => 'ro',
  isa => 'ArrayRef[Str]',
  predicate => 'has_langfuse_tags',
);


has langfuse_release => (
  is => 'ro',
  isa => 'Str',
  predicate => 'has_langfuse_release',
);


has langfuse_version => (
  is => 'ro',
  isa => 'Str',
  predicate => 'has_langfuse_version',
);


has langfuse_metadata => (
  is => 'ro',
  isa => 'HashRef',
  predicate => 'has_langfuse_metadata',
);


has raider_mcp => (
  is => 'ro',
  predicate => 'has_raider_mcp',
);


has on_ask_user => (

lib/Langertha/Raider.pm  view on Meta::CPAN

);

has embedding_engine => (
  is => 'ro',
  predicate => 'has_embedding_engine',
);


has no_session_embeddings => (
  is => 'ro',
  default => sub { 0 },
);


has _session_embeddings => (
  is => 'ro',
  isa => 'ArrayRef',
  default => sub { [] },
);

sub BUILD {
  my ( $self ) = @_;

  # Auto-activate catalog MCPs with auto => 1
  for my $name (keys %{$self->mcp_catalog}) {
    my $entry = $self->mcp_catalog->{$name};
    if ($entry->{auto}) {
      $self->_active_catalog_mcps->{$name} = $entry->{server};
    }
  }
}

sub clear_history {
  my ( $self ) = @_;
  $self->history([]);
  splice @{$self->_injections};
  return $self;
}


sub add_history {
  my ( $self, $role, $content ) = @_;
  push @{$self->history}, { role => $role, content => $content };
  return $self;
}


sub inject {
  my ( $self, @messages ) = @_;
  push @{$self->_injections}, @messages;
  return $self;
}


sub reset {
  my ( $self ) = @_;
  $self->clear_history;
  $self->_clear_active_engine;
  $self->_active_engine_name(undef);
  $self->metrics({
    raids => 0, iterations => 0, tool_calls => 0, time_ms => 0,
  });
  return $self;
}


sub active_engine {
  my ($self) = @_;
  return $self->_has_active_engine ? $self->_active_engine : $self->engine;
}


sub active_engine_name {
  my ($self) = @_;
  return $self->_active_engine_name;
}


sub switch_engine {
  my ($self, $name) = @_;
  croak "Engine '$name' not found in engine_catalog"
    unless exists $self->engine_catalog->{$name};
  my $entry = $self->engine_catalog->{$name};
  my $engine = $entry->{engine} // $self->engine;
  $self->_active_engine($engine);
  $self->_active_engine_name($name);
  $self->_tools_dirty(1);
  return $engine;
}


sub reset_engine {
  my ($self) = @_;
  $self->_clear_active_engine;
  $self->_active_engine_name(undef);
  $self->_tools_dirty(1);
  return $self->engine;
}


sub engine_info {
  my ($self) = @_;
  my $engine = $self->active_engine;
  return {
    name  => $self->_active_engine_name // 'default',
    class => ref $engine,
    model => $engine->can('chat_model') ? $engine->chat_model : undef,
  };
}


sub list_engines {
  my ($self) = @_;
  my %list;
  $list{default} = {
    engine => $self->engine,
    active => !$self->_has_active_engine,
  };
  for my $name (keys %{$self->engine_catalog}) {
    my $entry = $self->engine_catalog->{$name};
    $list{$name} = {

lib/Langertha/Raider.pm  view on Meta::CPAN

      history_length => scalar @{$self->history},
    );
    if ($self->has_langfuse_metadata) {
      %trace_meta = (%trace_meta, %{$self->langfuse_metadata});
    }
    $trace_id = $engine->langfuse_trace(
      name     => $self->langfuse_trace_name,
      input    => \@messages,
      metadata => \%trace_meta,
      $self->has_langfuse_user_id    ? ( user_id    => $self->langfuse_user_id )    : (),
      $self->has_langfuse_session_id ? ( session_id => $self->langfuse_session_id ) : (),
      $self->has_langfuse_tags       ? ( tags       => $self->langfuse_tags )       : (),
      $self->has_langfuse_release    ? ( release    => $self->langfuse_release )    : (),
      $self->has_langfuse_version    ? ( version    => $self->langfuse_version )    : (),
    );
  }

  # Auto-compress if threshold exceeded
  if ($self->has_max_context_tokens && $self->has_last_prompt_tokens
      && $self->_last_prompt_tokens > $self->max_context_tokens * $self->context_compress_threshold) {
    await $self->compress_history_f();
  }

  # Plugin hook: transform input messages before raid
  for my $plugin (@{$self->_plugin_instances}) {
    @messages = @{await $plugin->plugin_before_raid(\@messages)};
  }

  # Initialize inline MCP if tools defined
  await $self->_initialize_inline_mcp_f;

  # Gather tools from all sources
  my ( $all_tools, $tool_server_map ) = await $self->_gather_tools_f;

  croak "No tools available (configure MCP servers, inline tools, or raider_mcp)"
    unless @$all_tools;

  my $formatted_tools = $engine->format_tools($all_tools);
  my $model_params = $langfuse ? $self->_langfuse_model_parameters($engine) : undef;

  # Build new user messages
  my @user_msgs = map {
    ref $_ ? $_ : { role => 'user', content => $_ }
  } @messages;

  # Push user messages to session_history
  $self->_push_session_history(@user_msgs);

  # Build full conversation: mission + history + new messages
  my @conversation;
  push @conversation, { role => 'system', content => $self->mission }
    if $self->has_mission;
  push @conversation, @{$self->history};
  push @conversation, @user_msgs;

  # Plugin hook: transform assembled conversation
  for my $plugin (@{$self->_plugin_instances}) {
    @conversation = @{await $plugin->plugin_build_conversation(\@conversation)};
  }

  my $raid_iterations = 0;
  my $raid_tool_calls = 0;
  my @injected_history;

  # Package loop state for potential continuation
  my $state = {
    engine           => $engine,
    t0               => $t0,
    langfuse         => $langfuse,
    trace_id         => $trace_id,
    tool_server_map  => $tool_server_map,
    formatted_tools  => $formatted_tools,
    model_params     => $model_params,
    user_msgs        => \@user_msgs,
    conversation     => \@conversation,
    raid_iterations  => \$raid_iterations,
    raid_tool_calls  => \$raid_tool_calls,
    injected_history => \@injected_history,
  };

  return await $self->_run_raid_loop($state, 1);
}

async sub _run_raid_loop {
  my ( $self, $state, $start_iteration ) = @_;
  my $engine           = $state->{engine};
  my $langfuse         = $state->{langfuse};
  my $trace_id         = $state->{trace_id};
  my $tool_server_map  = $state->{tool_server_map};
  my $formatted_tools  = $state->{formatted_tools};
  my $model_params     = $state->{model_params};
  my $user_msgs        = $state->{user_msgs};
  my $conversation     = $state->{conversation};
  my $raid_iterations  = $state->{raid_iterations};
  my $raid_tool_calls  = $state->{raid_tool_calls};
  my $injected_history = $state->{injected_history};
  my $t0               = $state->{t0};

  for my $iteration ($start_iteration..$self->max_iterations) {
    $$raid_iterations++;

    # Re-gather tools if catalog/engine changed
    if ($self->_tools_dirty) {
      $engine = $self->active_engine;
      $state->{engine} = $engine;
      my ( $all_tools, $new_map ) = await $self->_gather_tools_f;
      $formatted_tools = $engine->format_tools($all_tools);
      $tool_server_map = $new_map;
      $state->{formatted_tools} = $formatted_tools;
      $state->{tool_server_map} = $tool_server_map;
      if ($langfuse) {
        $model_params = $self->_langfuse_model_parameters($engine);
        $state->{model_params} = $model_params;
      }

      $self->_tools_dirty(0);
    }

    # Drain injections for iterations 2+
    if ($iteration > $start_iteration || $start_iteration > 1) {
      my @injected;
      if (@{$self->_injections}) {
        push @injected, splice @{$self->_injections};
      }
      if ($self->has_on_iteration) {
        my $cb_msgs = $self->on_iteration->($self, $iteration);
        push @injected, @$cb_msgs if $cb_msgs && @$cb_msgs;
      }
      if (@injected) {
        my @msgs = map {
          ref $_ ? $_ : { role => 'user', content => $_ }
        } @injected;
        push @$conversation, @msgs;
        push @$injected_history, @msgs;
        $self->_push_session_history(@msgs);
      }
    }

    # Plugin hook: transform conversation before each LLM call
    for my $plugin (@{$self->_plugin_instances}) {
      $conversation = await $plugin->plugin_before_llm_call($conversation, $iteration);
    }

    my $iter_t0 = $langfuse ? $engine->_langfuse_timestamp : undef;

    # Langfuse: create iteration span
    my $iter_span_id;
    if ($langfuse) {
      $iter_span_id = $engine->langfuse_span(
        trace_id   => $trace_id,
        name       => "iteration-$iteration",
        start_time => $iter_t0,
      );
    }

    # Build and send the request
    my $request = $engine->build_tool_chat_request($conversation, $formatted_tools);

    my $response = await $engine->_async_http->do_request(request => $request);

    unless ($response->is_success) {
      die "".(ref $engine)." raid request failed: ".$response->status_line."\n".$response->content;
    }

    my $data = $engine->parse_response($response);

    # Plugin hook: inspect/transform LLM response
    for my $plugin (@{$self->_plugin_instances}) {
      $data = await $plugin->plugin_after_llm_response($data, $iteration);
    }

    # Track prompt tokens for auto-compression
    my $pt = $self->_extract_prompt_tokens($data);
    $self->_last_prompt_tokens($pt) if defined $pt;

    # Extract usage for Langfuse
    my $langfuse_usage = $langfuse ? $self->_langfuse_usage($data) : undef;

    # Extract tool calls
    my $tool_calls = $engine->response_tool_calls($data);

    # No tool calls means done — extract final text
    unless (@$tool_calls) {
      my $text = $engine->response_text_content($data);
      if ($engine->think_tag_filter) {
        ($text) = $engine->filter_think_content($text);
      }

      my $iter_t1 = $langfuse ? $engine->_langfuse_timestamp : undef;

      # Langfuse: generation nested under iteration span
      if ($langfuse) {
        $engine->langfuse_generation(
          trace_id              => $trace_id,
          parent_observation_id => $iter_span_id,
          name                  => 'llm-call',
          model                 => $engine->chat_model,
          input                 => $conversation,
          output                => $text,
          start_time            => $iter_t0,
          end_time              => $iter_t1,
          $langfuse_usage  ? ( usage            => $langfuse_usage )  : (),
          $model_params    ? ( model_parameters => $model_params )    : (),
        );

        # Close iteration span
        $engine->langfuse_update_span(
          id       => $iter_span_id,
          end_time => $iter_t1,
          output   => $text,
        );

        # Update trace with final output
        $engine->langfuse_update_trace(
          id     => $trace_id,
          output => $text,
        );
      }

      # Persist user messages, injections, and final assistant response in history
      push @{$self->history}, @$user_msgs;
      push @{$self->history}, @$injected_history if @$injected_history;
      push @{$self->history}, { role => 'assistant', content => $text };

      # Push final assistant response to session_history
      $self->_push_session_history({ role => 'assistant', content => $text });

      # Update metrics
      my $elapsed = tv_interval($t0) * 1000;
      my $m = $self->metrics;
      $m->{raids}++;
      $m->{iterations}  += $$raid_iterations;
      $m->{tool_calls}  += $$raid_tool_calls;
      $m->{time_ms}     += $elapsed;

      my $result = Langertha::Raider::Result->new(type => 'final', text => $text);

      # Plugin hook: transform final result before return
      for my $plugin (@{$self->_plugin_instances}) {
        $result = await $plugin->plugin_after_raid($result);
      }

      return $result;
    }

    # Langfuse: generation for the LLM call that produced tool calls
    my $post_llm_t = $langfuse ? $engine->_langfuse_timestamp : undef;
    if ($langfuse) {
      $engine->langfuse_generation(
        trace_id              => $trace_id,
        parent_observation_id => $iter_span_id,
        name                  => 'llm-call',
        model                 => $engine->chat_model,
        input                 => $conversation,
        output                => $engine->json->encode([map {
          ($engine->extract_tool_call($_))[0]
        } @$tool_calls]),
        start_time            => $iter_t0,
        end_time              => $post_llm_t,
        $langfuse_usage  ? ( usage            => $langfuse_usage )  : (),
        $model_params    ? ( model_parameters => $model_params )    : (),
      );
    }

    # Execute each tool call
    my @results;
    for my $tc (@$tool_calls) {
      my ( $name, $input ) = $engine->extract_tool_call($tc);

      # Plugin hook: inspect/transform before tool execution
      my @plugin_tc = await $self->_plugin_pipeline_tool_call($name, $input);
      unless (@plugin_tc) {
        # Plugin returned empty list — skip this tool call
        my $skip_result = {
          content => [{ type => 'text', text => "Tool call '$name' was skipped by plugin." }],
        };
        push @results, { tool_call => $tc, result => $skip_result };
        $$raid_tool_calls++;
        next;
      }
      ( $name, $input ) = @plugin_tc;

      my $tool_t0 = $langfuse ? $engine->_langfuse_timestamp : undef;

      # Check for virtual self-tools
      if ($name =~ /^raider_/ && $self->has_raider_mcp) {
        my $self_result = $self->_execute_self_tool($name, $input);

        # Handle interactive self-tool results
        if ($self_result->{type} eq 'question' || $self_result->{type} eq 'pause') {
          # Save continuation state for respond_f
          $self->_continuation({
            state          => $state,
            iteration      => $iteration,
            data           => $data,
            pending_tc     => $tc,
            remaining_tcs  => [grep { $_ != $tc } @$tool_calls],
            results_so_far => \@results,
            iter_span_id   => $iter_span_id,
          });

          if ($self_result->{type} eq 'question') {
            return Langertha::Raider::Result->new(
              type    => 'question',
              content => $self_result->{question},
              $self_result->{options} ? (options => $self_result->{options}) : (),
            );
          } else {
            return Langertha::Raider::Result->new(
              type    => 'pause',
              content => $self_result->{reason},
            );
          }
        }

        if ($self_result->{type} eq 'abort') {
          # Finalize metrics before aborting
          my $elapsed = tv_interval($t0) * 1000;
          my $m = $self->metrics;
          $m->{iterations}  += $$raid_iterations;
          $m->{tool_calls}  += $$raid_tool_calls;
          $m->{time_ms}     += $elapsed;

          return Langertha::Raider::Result->new(
            type    => 'abort',
            content => $self_result->{reason},
          );
        }

        if ($self_result->{type} eq 'wait') {
          my $loop = $engine->_async_http->loop;
          await $loop->delay_future(after => $self_result->{seconds});
          my $result = {
            content => [{ type => 'text', text => "Waited $self_result->{seconds} seconds." }],
          };

          if ($langfuse) {
            $engine->langfuse_span(
              trace_id              => $trace_id,
              parent_observation_id => $iter_span_id,
              name                  => "tool: $name",
              input                 => $input,
              output                => "Waited $self_result->{seconds} seconds.",
              start_time            => $tool_t0,
              end_time              => $engine->_langfuse_timestamp,
            );
          }

          push @results, { tool_call => $tc, result => $result };
          $$raid_tool_calls++;
          next;
        }

        # type eq 'result' — normal self-tool result
        my $result = $self_result;

        # Plugin hook: transform tool result
        for my $plugin (@{$self->_plugin_instances}) {
          $result = await $plugin->plugin_after_tool_call($name, $input, $result);
        }

        if ($langfuse) {
          my $tool_output = join('', map { $_->{text} // '' } @{$result->{content} // []});
          $engine->langfuse_span(
            trace_id              => $trace_id,
            parent_observation_id => $iter_span_id,
            name                  => "tool: $name",
            input                 => $input,
            output                => $tool_output,
            start_time            => $tool_t0,
            end_time              => $engine->_langfuse_timestamp,
          );
        }

        push @results, { tool_call => $tc, result => $result };
        $$raid_tool_calls++;
        next;
      }

      # Normal MCP tool call
      my $mcp = $tool_server_map->{$name}
        or die "Tool '$name' not found on any MCP server";

      my $result = await $mcp->call_tool($name, $input)->else(sub {
        my ( $error ) = @_;
        Future->done({
          content => [{ type => 'text', text => "Error calling tool '$name': $error" }],
          isError => JSON::MaybeXS->true,
        });
      });

      # Plugin hook: transform tool result
      for my $plugin (@{$self->_plugin_instances}) {
        $result = await $plugin->plugin_after_tool_call($name, $input, $result);
      }

      # Langfuse: span for each tool call, nested under iteration span
      if ($langfuse) {
        my $tool_output = join('', map { $_->{text} // '' } @{$result->{content} // []});
        $engine->langfuse_span(
          trace_id              => $trace_id,
          parent_observation_id => $iter_span_id,
          name                  => "tool: $name",
          input                 => $input,
          output                => $tool_output,
          start_time            => $tool_t0,
          end_time              => $engine->_langfuse_timestamp,
          $result->{isError} ? ( level => 'ERROR' ) : (),
        );
      }

      push @results, { tool_call => $tc, result => $result };
      $$raid_tool_calls++;
    }

    # Langfuse: close iteration span after tools complete
    if ($langfuse) {
      $engine->langfuse_update_span(
        id       => $iter_span_id,
        end_time => $engine->_langfuse_timestamp,
        metadata => {
          tool_calls => scalar @$tool_calls,
          tools_used => [map {
            ($engine->extract_tool_call($_->{tool_call}))[0]
          } @results],
        },
      );
    }

    # Append assistant + tool results to conversation and session_history
    my @tool_msgs = $engine->format_tool_results($data, \@results);
    push @$conversation, @tool_msgs;
    $self->_push_session_history(@tool_msgs);
  }

  die "Raider tool loop exceeded ".$self->max_iterations." iterations";
}

async sub respond_f {
  my ( $self, $answer ) = @_;
  croak "No pending interaction — call raid_f first"
    unless $self->has_continuation;

  my $cont = $self->_continuation;
  $self->clear_continuation;

  my $state      = $cont->{state};
  my $data       = $cont->{data};
  my $pending_tc = $cont->{pending_tc};
  my @results    = @{$cont->{results_so_far}};
  my $engine     = $state->{engine};

  # Add the answer as the tool result for the pending self-tool call
  my $answer_result = {
    content => [{ type => 'text', text => "$answer" }],
  };
  push @results, { tool_call => $pending_tc, result => $answer_result };
  ${$state->{raid_tool_calls}}++;

  # Execute remaining tool calls from the same batch
  for my $tc (@{$cont->{remaining_tcs}}) {
    my ( $name, $input ) = $engine->extract_tool_call($tc);

    if ($name =~ /^raider_/ && $self->has_raider_mcp) {
      my $self_result = $self->_execute_self_tool($name, $input);
      if ($self_result->{type} eq 'result') {
        for my $plugin (@{$self->_plugin_instances}) {
          $self_result = await $plugin->plugin_after_tool_call($name, $input, $self_result);
        }
        push @results, { tool_call => $tc, result => $self_result };
        ${$state->{raid_tool_calls}}++;
      }
      # For simplicity, skip interactive self-tools in remaining batch
      next;
    }

    my $mcp = $state->{tool_server_map}{$name}
      or die "Tool '$name' not found on any MCP server";

    my $result = await $mcp->call_tool($name, $input)->else(sub {
      my ( $error ) = @_;
      Future->done({
        content => [{ type => 'text', text => "Error calling tool '$name': $error" }],
        isError => JSON::MaybeXS->true,
      });
    });

    for my $plugin (@{$self->_plugin_instances}) {
      $result = await $plugin->plugin_after_tool_call($name, $input, $result);
    }

    push @results, { tool_call => $tc, result => $result };
    ${$state->{raid_tool_calls}}++;
  }

  # Close iteration span if Langfuse

lib/Langertha/Raider.pm  view on Meta::CPAN

        my $r2 = await $raider->raid_f('Tell me more about the first file you found.');
        say $r2;

        # Check metrics
        my $m = $raider->metrics;
        say "Raids: $m->{raids}, Tool calls: $m->{tool_calls}, Time: $m->{time_ms}ms";

        # Reset for a fresh conversation
        $raider->clear_history;
    }

    main()->get;

=head1 DESCRIPTION

Langertha::Raider is an autonomous agent that wraps a Langertha engine
with MCP tools. It maintains conversation history across multiple
interactions (raids), enabling multi-turn conversations where the LLM
can reference prior context.

B<Key features:>

=over 4

=item * Conversation history persisted across raids

=item * Mission (system prompt) separate from engine's system_prompt

=item * Automatic MCP tool calling loop

=item * Cumulative metrics tracking

=item * Hermes tool calling support (inherited from engine)

=item * Mid-raid context injection via C<inject()> and C<on_iteration>

=back

B<History management:> Only user messages and final assistant text
responses are persisted in history. Intermediate tool-call messages
(assistant tool requests and tool results) are NOT persisted, preventing
token bloat across long conversations.

=head2 engine

Required. A Langertha engine instance with MCP servers configured.
The engine must compose L<Langertha::Role::Tools>.

=head2 mission

Optional system prompt for the Raider. This is separate from the
engine's own C<system_prompt> — the Raider's mission takes precedence
and is prepended to every conversation.

=head2 history

ArrayRef of message hashes representing the conversation history.
Automatically managed by C<raid>/C<raid_f>. Can be inspected or
manually set.

=head2 max_iterations

Maximum number of tool-calling round trips per raid. Defaults to C<10>.

=head2 max_context_tokens

Optional. Enables auto-compression when set. When prompt token usage
exceeds C<context_compress_threshold * max_context_tokens>, the working
history is summarized via LLM before the next raid.

=head2 context_compress_threshold

Fraction of C<max_context_tokens> that triggers compression. Defaults
to C<0.75> (75%).

=head2 compression_prompt

System prompt used for history summarization. Customizable. The default
instructs the LLM to preserve key facts, decisions, and context.

=head2 compression_engine

Optional separate engine for compression (e.g. a cheaper model).
Falls back to C<engine> when not set.

=head2 session_history

Full chronological archive of ALL messages including tool calls and
results. Never auto-compressed. Persists across C<clear_history> and
C<reset>. Only cleared manually via C<< $raider->session_history([]) >>.

=head2 on_iteration

Optional CodeRef called before each LLM call (iterations 2+). Receives
C<($raider, $iteration)> and returns an arrayref of messages to inject,
or undef/empty to skip.

    my $raider = Langertha::Raider->new(
        engine => $engine,
        on_iteration => sub {
            my ($raider, $iteration) = @_;
            return ['Check the error log'] if $iteration == 3;
            return;
        },
    );

=head2 metrics

HashRef of cumulative metrics across all raids:

    {
        raids      => 3,       # Number of completed raids
        iterations => 7,       # Total LLM round trips
        tool_calls => 12,      # Total tool invocations
        time_ms    => 4500.2,  # Total wall-clock time in milliseconds
    }

=head2 langfuse_trace_name

Name for the Langfuse trace created per raid. Defaults to C<'raid'>.

=head2 langfuse_user_id

Optional user ID passed to the Langfuse trace.

=head2 langfuse_session_id

Optional session ID passed to the Langfuse trace. Use this to group
multiple raids into a single Langfuse session.

=head2 langfuse_tags

Optional tags (ArrayRef[Str]) passed to the Langfuse trace.

=head2 langfuse_release

Optional release identifier passed to the Langfuse trace.

=head2 langfuse_version

Optional version string passed to the Langfuse trace.

=head2 langfuse_metadata

Optional metadata HashRef merged into the Langfuse trace metadata
(alongside auto-generated fields like mission and history_length).

=head2 raider_mcp

Enables virtual self-tools that the LLM can call to interact with the
Raider itself. Set to C<1> to enable all self-tools, or pass a HashRef
to enable selectively:

    raider_mcp => 1                               # all self-tools
    raider_mcp => { ask_user => 1, pause => 1 }   # only these

Available self-tools: C<ask_user>, C<wait>, C<wait_for>, C<pause>,
C<abort>, C<session_history>, C<manage_mcps>, C<switch_engine>.

=head2 on_ask_user

Optional callback for the C<raider_ask_user> self-tool. Receives
C<($question, $options)> and must return an answer string. When not set,
the raid pauses and returns a C<question> Result that can be continued
with L</respond_f>.

=head2 on_pause

Optional callback for the C<raider_pause> self-tool. Receives C<($reason)>.
When not set, the raid pauses and returns a C<pause> Result.

=head2 on_wait_for

lib/Langertha/Raider.pm  view on Meta::CPAN


=head2 engine_catalog

HashRef of named engines available for runtime switching via C<switch_engine>.

    engine_catalog => {
        fast  => { engine => $groq,      description => 'Fast inference' },
        smart => { engine => $anthropic,  description => 'Complex reasoning' },
        code  => { engine => $deepseek,   description => 'Code generation' },
    }

Entries without an C<engine> key refer to the default engine (the one passed
as C<engine> at construction). This lets you give the default engine a named
catalog entry with a description:

    engine_catalog => {
        sonnet => { description => 'Balanced model for everyday tasks' },
        fast   => { engine => $groq,  description => 'Fast inference' },
        smart  => { engine => $opus,  description => 'Complex reasoning' },
    }

The LLM always sees a C<default> entry (reset to original) plus all catalog
keys in the C<raider_switch_engine> tool enum.

Use C<switch_engine>, C<reset_engine>, C<active_engine>, and C<engine_info>
to control which engine is used during raids.

=head2 embedding_engine

Optional engine with L<Langertha::Role::Embedding> for semantic history search.
When not set, auto-detects if the main C<engine> supports embeddings.
Set L</no_session_embeddings> to disable auto-detection.

=head2 no_session_embeddings

When true, disables automatic embedding computation for session history
entries. Useful when the engine supports embeddings but calling it would
cause issues (e.g. self-referencing proxy deadlock).

=head2 clear_history

    $raider->clear_history;

Clears conversation history and pending injections while preserving metrics.

=head2 add_history

    $raider->add_history('user', 'Hello');
    $raider->add_history('assistant', 'Hi there!');

Appends a message to the conversation history. Useful for replaying
persisted messages into a fresh Raider instance.

=head2 inject

    $raider->inject('Also check the test files');
    $raider->inject({ role => 'user', content => 'Focus on .pm files' });

Queues messages to be injected into the conversation at the next iteration.
Strings are automatically wrapped as user messages. The Raider drains the
queue before each LLM call (iterations 2+).

=head2 reset

    $raider->reset;

Clears conversation history, metrics, and resets to the default engine.

=head2 active_engine

    my $engine = $raider->active_engine;

Returns the currently active engine. If C<switch_engine> was called, returns
the catalog engine; otherwise returns the default C<engine>.

=head2 active_engine_name

    my $name = $raider->active_engine_name;  # 'smart' or undef

Returns the name of the currently active catalog engine, or C<undef> if using
the default engine.

=head2 switch_engine

    $raider->switch_engine('smart');

Switches to a named engine from the C<engine_catalog>. Sets C<_tools_dirty>
so the raid loop re-gathers and re-formats tools for the new engine.
Croaks if the name is not in the catalog.

=head2 reset_engine

    $raider->reset_engine;

Switches back to the default engine (the one passed at construction).

=head2 engine_info

    my $info = $raider->engine_info;
    # { name => 'smart', class => 'Langertha::Engine::Anthropic', model => 'claude-sonnet-4-6' }

Returns a hashref with the active engine's name, class, and model.

=head2 list_engines

    my $engines = $raider->list_engines;

Returns a hashref of all available engines (default + catalog entries),
each with C<engine>, C<description> (if from catalog), and C<active> flag.

=head2 add_engine

    $raider->add_engine('vision', engine => $vision_engine, description => 'Vision model');
    $raider->add_engine('main', description => 'Default model for general tasks');

Adds a new engine to the catalog at runtime. If C<engine> is omitted, the
entry refers to the default engine. The LLM will see it in the
C<raider_switch_engine> tool after the next tool re-gather.

=head2 remove_engine



( run in 0.468 second using v1.01-cache-2.11-cpan-71847e10f99 )