LLM::Agent::Revival

NAME

LLM::Agent::Revival - bring an interrupted run back from its own transcript

SYNOPSIS


use LLM::Agent::Revival;

# Is there anything to revive here? Reads the file, writes nothing.
my %found = LLM::Agent::Revival.inspect(
    path => $child-path, max-revivals => 2,
);
say %found<reason> unless %found<revivable>;   # 'finished', 'revival-cap', ...

# Yes: repair the interrupted call, tell the run what happened, and hand
# back everything needed to start it again as ITSELF.
my %revived = LLM::Agent::Revival.revive(
    path => $child-path, agent-id => 'researcher-1', max-revivals => 2,
);

if %revived<ok> {
    my $loop = LLM::Agent::Loop.new(
        :@backends, :$provider, session => %revived<session>,
    );
    my $run = $loop.run([|%revived<messages>]);

    # ...and the composer takes it back under its ORIGINAL id, so the
    # parent's outstanding `task_wait` finds it rather than a stranger.
    $composer.revive-child(
        agent-id => %revived<agent-id>,
        agent-type => %revived<agent-type>,
        label      => %revived<label>,
        call-id    => $spawn-envelope<call-id>,
        run        => $run,
        session-path => %revived<path>.Str,
    );
}

DESCRIPTION

A process that dies does not stop being a conversation. Its transcript is on disk, complete to the last line that returned (see LLM::Agent::Session), and everything needed to carry it on is in there: the messages, the tool calls that were in flight, and — for a child of a delegating agent — the session-meta its host wrote when it started it.

This class is the engine half of picking that up: given one transcript, it decides whether there is a run to revive, repairs what the interruption left open, tells the run what happened to it, and hands back the pieces. It deliberately does not scan a directory, ask a human, or build a loop. Which transcripts to look at is an application's question (a child lives in <session>.agents/ only because an app put it there), whether to spend the tokens is a human's — a crash that reproduces would otherwise revive the same four agents for ever — and how a run is wired is the app's own, exactly as it is for a first spawn.

The three things an interruption leaves behind

A tool call that was in flight

tool-dispatched with no tool-settled means the call was handed over and the process died before it heard back (or heard back and died before it could say so). It is answered as a failure, in the conversation, with a message that says what happened and what to do about it:


The previous session ended while this tool call was in flight, so it was
interrupted. Whether it took effect is unknown, and it has NOT been
retried: check the state of anything it was changing — a file it was
writing may have been left half-written — before assuming either way.

It is never replayed, and the idempotency the dispatch envelope records does not change that. A read-only call looks safe to run again right up until the moment the thing it read has been changed by the half of the batch that did land; more to the point, a model that is told "this may have half-happened" can go and look, and a replay heuristic cannot. One honest failure covers every case, and the model decides.

The operation itself settles failed with reason => 'interrupted' . Two other shapes settle without a message, because the conversation already accounts for them:

What the transcript has | Outcome Reason
dispatch, no settle, no tool message | failed interrupted
dispatch, no settle, a tool message | outcome-unknown completed-unpersisted
dispatch for a call no turn still has| outcome-unknown orphaned

The second is the loop's write order paying off (the tool message is written before the settle, so a crash between them is distinguishable from a call still running); the third is a compaction having spliced the turn away since.

A run that does not know any of this

Every revived run is told, in the conversation, immediately before it starts thinking again:


[background event] Automated notification from the system, not the user
speaking. The previous session ended while this run was working, and the
run has been resumed: everything above is its own conversation, up to the
moment it stopped. Work it had not finished is still unfinished.

It opens with the same marker and the same first sentence as LLM::Agent::Subagents' BACKGROUND-FRAME, and for the same reason: a model that has learned what [background event] means from one producer must not have to learn it again from another, and a harness notification a model has not been taught about is a notification it treats as injected content. It is an ordinary user turn carrying injected => 'revival' in its transcript extras, which is the durable marker the loop's own background deliverables use — so a second pass over the file can tell a run that has already been told from one that has not.

A parent that is still waiting

Nothing in here touches the parent. Its task_wait calls stay pending rather than being settled as unknown, which is what LLM::Agent::Loop's resume-tool-calls exists to make possible: the joins are answered by the children that are starting again, under the call ids they were made with. See that class's "the three states of a tool-call tail".

What counts as revivable

A transcript whose last message is an assistant turn asking for no tools is a run that finished: it said its piece, and reviving it would start a second run that had nothing to do. Everything else — a turn with tool calls nothing answered, a tail of tool results the model never got to read, a user turn that was still being thought about — is a run that was interrupted mid-work, and is revivable.

The cap, and why it is a parameter

max-revivals bounds how many times one transcript may be revived, and it is counted from the transcript itself: every revival appends a run-revived envelope, so the count survives the process that made it. A revival past the cap is refused with reason => 'revival-cap' and nothing is written.

It is a parameter rather than a constant because the policy belongs to the application: a harness that asks a human every time can afford a generous cap, an unattended one cannot, and a crash that reproduces exactly is otherwise an unbounded bill with nothing but a human's attention between it and the card. max-revivals => 0 refuses every revival, which is how a host turns the whole mechanism off without special-casing it.

Nothing throws

inspect never writes and never throws: a transcript that cannot be read is a fact to report ( reason => 'unreadable' ), the same posture LLM::Agent::Session's peek takes, and for the same reason — the caller is a repair, reading somebody else's file, possibly while it is still being written.

revive throws only for a caller that asked for something impossible (a path and a session at once, neither, or a negative cap). Everything about the transcript's own state — finished, capped, unreadable — comes back as ok => False with a reason, because none of those are programmer errors and all of them have to be shown to a human.

SEE ALSO

LLM::Agent::Session (the transcript and its crash repair), LLM::Agent::Loop (resume-tool-calls, the pending-again seed state), LLM::Agent::Subagents (revive-child, which takes a revived run back under its original id).

  • one tool message per interrupted call, carrying INTERRUPTED-MESSAGE and is-error => True ;

  • one settle per open dispatch (failed/interrupted, outcome-unknown/completed-unpersisted, or outcome-unknown/orphaned — see the module Pod);

  • the resume notice, as a user turn marked injected => 'revival' ;

  • one run-revived envelope, which is what the cap counts.

LLM::Agent v0.6.3

a streaming agent loop: tools, retry, fallback, a durable

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Digest::SHA256::Native:ver<1.0.0+>:auth<zef:bduggan>LLM::Chat:ver<0.10.3+>:auth<zef:apogee>MCP::Client:ver<0.6.1+>:auth<zef:apogee>JSONL:ver<0.1.6+>:auth<zef:apogee>JSON::Fast:ver<0.21.0+>:auth<zef:timo>UUID::V4:ver<1.0.0+>:auth<zef:masukomi>

Test Dependencies

Provides

  • LLM::Agent
  • LLM::Agent::Artifacts
  • LLM::Agent::Canonical
  • LLM::Agent::Compactor
  • LLM::Agent::CompletionBus
  • LLM::Agent::Event
  • LLM::Agent::Loop
  • LLM::Agent::Prompt
  • LLM::Agent::RequestBudget
  • LLM::Agent::Revival
  • LLM::Agent::Run
  • LLM::Agent::RunContext
  • LLM::Agent::Session
  • LLM::Agent::Subagents
  • LLM::Agent::Test::Kit
  • LLM::Agent::TokenCount
  • LLM::Agent::ToolOperation
  • LLM::Agent::ToolRouting

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.