Langertha

 view release on metacpan or  search on metacpan

ex/raider_run.pl  view on Meta::CPAN

  my $haiku = Langertha::Engine::Anthropic->new(
    api_key => $api_key,
    model   => 'claude-haiku-4-5-20251001',
  );

  info("default (Sonnet) " . colored('90', $sonnet->chat_model) . " - main engine, active at start");
  info("fast    (Haiku)  " . colored('90', $haiku->chat_model) . "  - in engine_catalog");
  info("smart   (Sonnet) " . colored('90', $sonnet->chat_model) . " - in engine_catalog");
  info("");
  info("Engines have NO mcp_servers — all tools come from the MCP catalog.");
  info("The LLM can switch engines via raider_switch_engine.");

  # ── 5. Background "build" — creates report file after 4 seconds ──

  banner("SETUP: Background Build");

  unlink $report_file;

  $loop->add(IO::Async::Timer::Countdown->new(
    delay     => 4,
    on_expire => sub {
      open my $fh, '>', $report_file or return;
      print $fh "Build OK: 42 modules compiled, 0 errors\nTimestamp: " . localtime() . "\n";
      close $fh;
      printf "\n  %s\n", colored('32', "[BACKGROUND] Build report written to $report_file");
    },
  )->start);

  info("Timer started: $report_file will appear in 4 seconds.");
  info("The LLM will use raider_wait + raider_wait_for to check for it.");

  # ── 6. Self-tools overview ──

  banner("SETUP: Raider Self-Tools");
  info("raider_switch_engine  - switch between engine catalog entries");
  info("raider_manage_mcps    - list/activate/deactivate MCP catalog entries");
  info("raider_wait           - wait N seconds");
  info("raider_wait_for       - wait for external condition (file_exists)");
  info("raider_ask_user       - ask user a question (simulated in demo)");
  info("raider_session_history- query the full session history");
  info("raider_pause          - pause execution (available but not used)");
  info("raider_abort          - abort the raid (available but not used)");

  # ── 7. Simulated user answers ──

  my @simulated_answers = (
    "Yes, run the tests with prove -l t/ and tell me how it went.",
    "Wrap it up.",
  );
  my $answer_idx = 0;

  # ── 8. State tracking for change detection ──

  my $prev_engine = 'default';
  my %prev_active_mcps;

  # ── 9. Raider ──

  my $raider = Langertha::Raider->new(
    engine  => $sonnet,
    max_iterations => 25,
    engine_catalog => {
      fast  => { engine => $haiku,  description => 'Fast model for quick tasks' },
      smart => { engine => $sonnet, description => 'Smart model for analysis' },
    },
    mcp_catalog => {
      shell => { server => $shell_mcp, description => 'Shell command execution (run tool)' },
      notes => { server => $notes_mcp, description => 'Notebook for saving findings' },
    },
    raider_mcp => 1,
    mission => join("\n",
      'You are a build engineer. You start with NO tools except self-tools.',
      'You must activate MCP servers from the catalog to get tools.',
      'Follow these phases IN ORDER. Do not skip phases.',
      '',
      'PHASE 1 — Bootstrap:',
      '  You have no shell yet! First get your tools:',
      '  Use raider_manage_mcps action "list" to see available MCP servers.',
      '  Use raider_manage_mcps action "activate" name "shell" to get the run tool.',
      '  Use raider_manage_mcps action "activate" name "notes" to get save_note/read_notes.',
      '',
      'PHASE 2 — Recon:',
      '  Now you have a shell. Run: uname -a',
      '  Run: ls',
      '  Run: wc -l lib/Langertha.pm',
      '',
      'PHASE 3 — Wait for build:',
      '  A background build is already running.',
      '  Use raider_wait for 3 seconds, reason "waiting for background build".',
      '  Use raider_wait_for condition "file_exists" args {"path":"/tmp/raider_build_report.txt"}.',
      '  Run: cat /tmp/raider_build_report.txt',
      '',
      'PHASE 4 — Switch engines and take notes:',
      '  Use raider_switch_engine name "fast" to switch to the fast model.',
      '  Use save_note to record the build result.',
      '  Use raider_switch_engine name "smart" to switch back.',
      '',
      'PHASE 5 — Ask user:',
      '  Use raider_ask_user question "Build succeeded. Should I run the tests too?"',
      '  Do what the user says.',
      '',
      'PHASE 6 — Review and report:',
      '  Use raider_session_history with last_n 5 to review recent activity.',
      '  Use read_notes to review your notebook.',
      '  Give a concise final status report.',
    ),
    on_ask_user => sub {
      my ($question, $options) = @_;
      sep();
      printf "  %s\n", colored('35', '[SELF-TOOL] raider_ask_user');
      info("  Q: $question");
      if ($options && @$options) {
        info("  Options: " . join(', ', @$options));
      }
      my $answer = $simulated_answers[$answer_idx] // "Wrap it up.";
      $answer_idx++;
      printf "  %s %s\n", colored('32', '  >>> Demo user:'), $answer;
      sep();
      return $answer;
    },
    on_wait_for => sub {

ex/raider_run.pl  view on Meta::CPAN

        $prev_engine = $cur_engine;
      }

      # Detect MCP catalog changes
      my %cur_active;
      for my $name (keys %{$raider->mcp_catalog}) {
        $cur_active{$name} = 1 if exists $raider->_active_catalog_mcps->{$name};
      }
      for my $name (keys %cur_active) {
        unless ($prev_active_mcps{$name}) {
          printf "  %s %s activated\n",
            colored('35', '[MCP CATALOG]'), colored('32', $name);
        }
      }
      for my $name (keys %prev_active_mcps) {
        unless ($cur_active{$name}) {
          printf "  %s %s deactivated\n",
            colored('35', '[MCP CATALOG]'), colored('33', $name);
        }
      }
      %prev_active_mcps = %cur_active;

      # Show iteration header with full state
      printf "\n  %s\n", colored('1', "=== ITERATION $iteration ===");
      show_state($raider);

      sleep 0.8;  # rate limit protection
      return;
    },
  );

  # ── 10. Raid ──

  banner("RAID: Full-Stack Build Engineer");
  info("Initial state:");
  show_state($raider);

  my $result = await $raider->raid_f(
    'Start the build engineer workflow. Follow all phases in your mission.'
  );

  # ── 11. Results ──

  printf "\n";
  banner("FINAL ANSWER");
  if ($result->is_final) {
    printf "%s\n", $result;
  } elsif ($result->is_question) {
    info("[RESULT] question: ${\$result->content}");
  } elsif ($result->is_pause) {
    info("[RESULT] pause: ${\$result->content}");
  } elsif ($result->is_abort) {
    info("[RESULT] abort: ${\$result->content}");
  }

  # ── 12. Metrics ──

  banner("METRICS");
  my $m = $raider->metrics;
  info(sprintf "Raids:      %d", $m->{raids});
  info(sprintf "Iterations: %d", $m->{iterations});
  info(sprintf "Tool calls: %d", $m->{tool_calls});
  info(sprintf "Total time: %.1f s", $m->{time_ms} / 1000);

  # ── 13. Notebook ──

  if (@notebook) {
    banner("NOTEBOOK (saved by LLM via notes MCP)");
    for my $note (@notebook) {
      info("[$note->{title}] $note->{text}");
    }
  }

  # ── 14. Session history ──

  banner("SESSION HISTORY");
  my @hist = @{$raider->session_history};
  for my $i (0 .. $#hist) {
    my $msg = $hist[$i];
    my $role = $msg->{role} // '?';
    my $content = $msg->{content} // '';
    if (ref $content eq 'ARRAY') {
      $content = join(' ', map {
        $_->{text} // "[" . ($_->{type} // '?') . "]"
      } @$content);
    }
    $content =~ s/\n/ /g;
    $content = substr($content, 0, 120) . '...' if length($content) > 120;
    printf "  %s %-10s %s\n", colored('90', sprintf('%02d', $i + 1)), $role, $content;
  }
  info("");
  info(sprintf "Session: %d messages | History: %d messages",
    scalar @hist, scalar @{$raider->history});

  # Cleanup
  unlink $report_file;
}

main()->get;



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