NAME
Langertha::Raider - Autonomous agent with conversation history and MCP tools
VERSION
version 0.503
SYNOPSIS
use IO::Async::Loop;
use Future::AsyncAwait;
use Net::Async::MCP;
use MCP::Server;
use Langertha::Engine::Anthropic;
use Langertha::Raider;
# Set up MCP server with tools
my $server = MCP::Server->new(name => 'demo', version => '1.0');
$server->tool(
name => 'list_files',
description => 'List files in a directory',
input_schema => {
type => 'object',
properties => { path => { type => 'string' } },
required => ['path'],
},
code => sub { $_[0]->text_result(join("\n", glob("$_[1]->{path}/*"))) },
);
my $loop = IO::Async::Loop->new;
my $mcp = Net::Async::MCP->new(server => $server);
$loop->add($mcp);
async sub main {
await $mcp->initialize;
my $engine = Langertha::Engine::Anthropic->new(
api_key => $ENV{ANTHROPIC_API_KEY},
mcp_servers => [$mcp],
);
my $raider = Langertha::Raider->new(
engine => $engine,
mission => 'You are a code explorer. Investigate files thoroughly.',
);
# First raid — uses tools, builds history
my $r1 = await $raider->raid_f('What files are in the current directory?');
say $r1;
# Second raid — has context from first conversation
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;
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.
Key features:
Conversation history persisted across raids
Mission (system prompt) separate from engine's system_prompt
Automatic MCP tool calling loop
Tool arguments checked against the tool's
inputSchemabefore it runs: a missingrequiredkey or a top-level property of the wrong type answers the call with an error result the model can correct, and the tool does not runCumulative metrics tracking
Hermes tool calling support (inherited from engine)
Mid-raid context injection via
inject()andon_iterationAutomatic context management — between-raid LLM history summary plus in-raid mechanical compaction, mission and skills preserved
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. The full turn-by-turn record, tool calls and results included, is kept separately in "session_history".
engine
Required. A Langertha engine instance with MCP servers configured. The engine must compose Langertha::Role::Tools.
mission
Optional system prompt for the Raider. This is separate from the engine's own system_prompt — the Raider's mission takes precedence and is prepended to every conversation.
history
ArrayRef of message hashes representing the conversation history. Automatically managed by raid/raid_f. Can be inspected or manually set.
max_iterations
Maximum number of tool-calling round trips per raid. Defaults to 10.
max_context_tokens
Optional. Enables auto-compression when set. When prompt token usage exceeds context_compress_threshold * max_context_tokens, the working history is summarized via LLM before the next raid, and within a single long raid the oldest tool-call exchanges are dropped from the in-flight conversation as the tool loop runs, oldest first, so one long-running mission cannot outgrow the context window. The mission (system prompt, including any activated skill instructions) is never compacted — an agent must not lose its instructions mid-raid.
context_compress_threshold
Fraction of max_context_tokens that triggers compression. Defaults to 0.75 (75%).
compression_prompt
System prompt used for history summarization. Customizable. The default instructs the LLM to preserve key facts, decisions, and context.
compression_engine
Optional separate engine for compression (e.g. a cheaper model). Falls back to engine when not set.
session_history
Full chronological archive of ALL messages including tool calls and results. Never auto-compressed. Persists across clear_history and reset. Cleared via clear_session_history, which also empties the internal _session_embeddings array to preserve their 1:1 invariant.
on_iteration
Optional CodeRef called before each LLM call (iterations 2+). Receives ($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;
},
);
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
}
langfuse_trace_name
Name for the Langfuse trace created per raid. Defaults to 'raid'.
langfuse_user_id
Optional user ID passed to the Langfuse trace.
langfuse_session_id
Optional session ID passed to the Langfuse trace. Use this to group multiple raids into a single Langfuse session.
langfuse_tags
Optional tags (ArrayRef[Str]) passed to the Langfuse trace.
langfuse_release
Optional release identifier passed to the Langfuse trace.
langfuse_version
Optional version string passed to the Langfuse trace.
langfuse_metadata
Optional metadata HashRef merged into the Langfuse trace metadata (alongside auto-generated fields like mission and history_length).
raider_mcp
Enables virtual self-tools that the LLM can call to interact with the Raider itself. Set to 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: ask_user, wait, wait_for, pause, abort, session_history, manage_mcps, switch_engine.
on_ask_user
Optional callback for the raider_ask_user self-tool. Receives ($question, $options) and must return an answer string. When not set, the raid pauses and returns a question Result that can be continued with "respond_f".
on_pause
Optional callback for the raider_pause self-tool. Receives ($reason). When not set, the raid pauses and returns a pause Result.
on_wait_for
Callback for the raider_wait_for self-tool. Receives ($condition, $args) and must return a result string. Required when the LLM uses raider_wait_for — will die if not set.
cancel
$raider->cancel;
Asks the raid in progress to stop (ADR 0009). It stops at its next safe point -- before the next model call, before the next tool call -- and a model response, tool call or raider_wait it waits for right then is abandoned: that future is cancelled, which aborts an HTTP request in flight on Net::Async::HTTP. A tool call cut off this way still gets its result, marked cancelled (text Tool call 'NAME' was cancelled., isError, cancelled => 1), through plugin_after_tool_call. Tool subprocesses are not signalled; that is the caller's to do. A tool call that ends with an error once the cancel is requested -- its subprocess ended by the caller -- counts as cut off too; one that succeeds keeps its result.
The raid then resolves with a cancelled Langertha::Raider::Result. Like a failed raid it adds nothing to "history"; the tool calls made so far stay in "session_history".
It only sets a flag and writes a byte to a pipe the event loop watches, so it is safe to call from a signal handler: the rest happens on the loop. A request made while no raid runs applies to the next one ("clear_cancel" drops it); each raid ends with no request pending ("cancel_requested").
cancel_requested
True from "cancel" until the raid it applies to has ended.
clear_cancel
$raider->clear_cancel;
Drops a cancel request that no raid has used yet ("cancel"). Every raid does this when it ends.
tools
Optional ArrayRef of inline tool definitions. Each entry is a HashRef with name, description, input_schema, and code keys — the same format as "tool" in MCP::Server. An internal MCP server is created automatically.
my $raider = Langertha::Raider->new(
engine => $engine,
tools => [{
name => 'greet',
description => 'Say hello',
input_schema => { type => 'object', properties => { name => { type => 'string' } } },
code => sub { $_[0]->text_result("Hello $_[1]->{name}!") },
}],
);
mcp_catalog
HashRef of named MCP servers available for dynamic activation. The LLM can use raider_manage_mcps to list, activate, and deactivate catalog entries.
mcp_catalog => {
database => { server => $db_mcp, description => 'Database tools', auto => 1 },
email => { server => $email_mcp, description => 'Email tools' },
}
Entries with auto => 1 are activated at construction time.
engine_catalog
HashRef of named engines available for runtime switching via 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 engine key refer to the default engine (the one passed as 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 default entry (reset to original) plus all catalog keys in the raider_switch_engine tool enum.
Use switch_engine, reset_engine, active_engine, and engine_info to control which engine is used during raids.
embedding_engine
Optional engine with Langertha::Role::Embedding for semantic history search. When not set, auto-detects if the main engine supports embeddings. Set "no_session_embeddings" to disable auto-detection.
Each session_history entry is embedded in the background through simple_embedding_f, so the raid never waits for it. Like every engine of the raider, the embedding engine must run on the same event loop as engine (see "raid_f").
no_session_embeddings
When true, no session history entry is embedded, and the search of raider_session_history falls back to a plain text match. Use it when the engine supports embeddings but they are not wanted, e.g. to save the extra request per message. With it set, embedding_engine is also left out of the event-loop check of "raid_f".
clear_history
$raider->clear_history;
Clears conversation history and pending injections while preserving metrics.
clear_session_history
$raider->clear_session_history;
Empties session_history and the matching _session_embeddings array in lock-step, so the 1:1 invariant both readers of session_history rely on is preserved. Use this instead of splicing session_history directly, which leaves _session_embeddings stale. Embedding requests still in flight for the cleared entries are cancelled.
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.
add_session_history
$raider->add_session_history(
{ role => 'user', content => 'Hello' },
{ role => 'tool', name => 'bash', content => 'README.md' },
);
Appends entries to session_history, keeping the session embeddings in step. Useful for replaying a persisted session into a fresh Raider instance; history is replayed separately with add_history.
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+).
reset
$raider->reset;
Clears conversation history, metrics, and resets to the default engine.
active_engine
my $engine = $raider->active_engine;
Returns the currently active engine. If switch_engine was called, returns the catalog engine; otherwise returns the default engine.
active_engine_name
my $name = $raider->active_engine_name; # 'smart' or undef
Returns the name of the currently active catalog engine, or undef if using the default engine.
switch_engine
$raider->switch_engine('smart');
Switches to a named engine from the engine_catalog. Sets _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.
reset_engine
$raider->reset_engine;
Switches back to the default engine (the one passed at construction).
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.
list_engines
my $engines = $raider->list_engines;
Returns a hashref of all available engines (default + catalog entries), each with engine, description (if from catalog), and active flag.
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 engine is omitted, the entry refers to the default engine. The LLM will see it in the raider_switch_engine tool after the next tool re-gather.
remove_engine
$raider->remove_engine('vision');
Removes an engine from the catalog. If the removed engine is currently active, automatically resets to the default engine. Croaks when the name is not in the catalog.
compress_history_f
my $summary = await $raider->compress_history_f;
Async. Summarizes the current working history via LLM and replaces it with the summary. Uses compression_engine if set, otherwise falls back to engine. A marker is added to session_history.
The summary is read through the engine's public chat_response (the parser "chat_f" in Langertha::Role::Chat uses), so an error carried in a 200 body croaks instead of silently producing an empty summary, and Gemini thought parts stay out of the text. An engine that lacks chat_response falls back to response_text_content.
compress_history
my $summary = $raider->compress_history;
Synchronous wrapper around compress_history_f.
register_session_history_tool
$raider->register_session_history_tool($mcp_server);
Registers a session_history MCP tool on the given server, allowing the LLM to query its own full session history. Supports query (text filter) and last_n (return last N messages) parameters.
run_f
my $result = await $raider->run_f($ctx); # a Langertha::RunContext, or plain input
The Langertha::Role::Runnable entry point that lets a raider be a step of a Langertha::Raid. Runs "raid_f" on the context's input (a string, or an ArrayRef of messages; plain input is wrapped in a new Langertha::RunContext). A final answer becomes the context's input and state->{last_output} for the next step; last_result_type, last_result and history are updated too. Returns the Langertha::Raider::Result with the context attached.
raid
my $response = $raider->raid(@messages);
Synchronous wrapper around raid_f. Sends messages, runs the tool loop, and returns the Langertha::Raider::Result, which stringifies to the final text. Updates history and metrics.
raid_f
my $result = await $raider->raid_f(@messages);
Async tool-calling conversation. Accepts the same message arguments as simple_chat (strings become user messages, hashrefs pass through). Returns a Future resolving to a Langertha::Raider::Result.
The result stringifies to the final text (backward compatible), but also provides type, is_final, is_question, is_pause, is_abort for programmatic handling of interactive self-tools, and is_cancelled for a raid stopped by "cancel".
All engines of a raider (engine, compression_engine, every engine_catalog engine, and embedding_engine unless no_session_embeddings is set) must run on the same event loop, since one raid is driven by one loop. An engine without a loop (the synchronous fallback) counts as the process-wide IO::Async::Loop->new. raid_f and respond_f fail right away, naming the engine, when one runs on another loop.
They also fail right away, naming the engine and the module, when a module Net::Async::HTTP needs to connect to an engine's url does not load: IO::Async::Internals::Connector, and for https IO::Async::SSL (which needs IO::Socket::SSL, Net::SSLeay and the system libssl). Sent anyway, the first request would fail and every later one to that host would wait forever.
respond_f
my $result = await $raider->respond_f($answer);
Continue a paused raid after a question or pause result. The answer is used as the tool result of the pausing self-tool call -- handed through plugin_after_tool_call like any tool result -- and the raid loop resumes. Returns the next Langertha::Raider::Result.
respond
my $result = $raider->respond($answer);
Synchronous wrapper around respond_f.
plugins
my $raider = Langertha::Raider->new(
plugins => ['Langfuse', 'MyApp::CustomPlugin'],
engine => $engine,
);
Arrayref of plugin names or Langertha::Plugin instances. Short names are resolved first to Langertha::Plugin::$name, then to LangerthaX::Plugin::$name. Fully qualified names (with ::) are used as-is.
A leading + (+My::Plugin) loads the class as named, without the prefix search, and a name may be followed by a HashRef of constructor arguments for that plugin.
Plugin instances are created automatically with host => $self. Extra constructor arguments can be passed via plugin_args.
Besides the tool-calling hooks every Langertha::Plugin has, a raider calls plugin_before_raid (the raid's messages), plugin_build_conversation (the conversation sent to the model) and plugin_after_raid (the final Langertha::Raider::Result); the contract is in Langertha::Plugin.
SEE ALSO
Langertha::Role::Tools - Lower-level single-turn tool calling
Langertha::Role::Langfuse - Observability integration (used by Raider)
Langertha::Role::SystemPrompt - Engine-level system prompt (Raider uses
missioninstead)Langertha::Plugin - Base role and documentation for Raider plugins
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-raider/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.