CompletionBus

NAME

LLM::Agent::CompletionBus - the outstanding background work a run will not end without, and the turns it comes back as

SYNOPSIS


use LLM::Agent::CompletionBus;

# One bus per conversation, not per run: a child that settles after its
# parent run ended is delivered to the NEXT run of the same conversation.
my $bus = LLM::Agent::CompletionBus.new(max-outstanding => 16);

my $loop = LLM::Agent::Loop.new(
    :@backends, provider => $subagents, completion-bus => $bus,
);

# ...from a composer, when a background operation starts:
my %admitted = $bus.open('reviewer-1', kind => 'subagent',
    label => 'review billing/');
# %admitted<ok> is False when the bus is already at max-outstanding, and
# %admitted<outstanding> / %admitted<max> are what the refusal says.

# ...and when it finishes, from whatever thread noticed:
$bus.settle('reviewer-1', deliverable => %(
    kind   => 'subagent-settled',
    head   => '[background event] ... The reviewer agent has finished:',
    body   => $child-answer,          # excerpt-seam routed by the loop
    tail   => 'Incorporate this result and continue your work.',
    extras => %( 'completion-of' => 'reviewer-1', 'call-id' => 'c1' ),
));

# The loop, and NOTHING else, drains:
for $bus.drain -> %item { ... }

DESCRIPTION

A background tool call answers twice: once immediately, with an acknowledgement that says the work has started, and once later, with what the work actually produced. This class is the thing in between — the register of work that has been acknowledged and not yet reported, and the queue of the reports themselves.

Two questions live here, and they are the only two:

  • Is the run allowed to end? A model that has stopped asking for tools normally means the run is over. With background work outstanding it means nothing of the sort: the answer it is waiting for has not arrived yet. quiet is the whole of that decision.

  • What has arrived since the last round? drain answers with every deliverable in the order it landed, and takes them off the bus.

Everything else — what a deliverable says, how a turn is framed, when a run parks and for how long — belongs to LLM::Agent::Loop and to the composer that opened the operation. This class holds no opinion about any of it.

Tracked operations, and untracked deliverables

There are two ways something reaches a run through this bus, and the difference is whether the run waits for it.

Kind How it arrives Does the run park for it?
tracked open ... settle YES, until it settles
untracked push no, ever

A tracked operation is one somebody promised an answer for. A task call that acknowledged immediately told the model "its answer will arrive later"; open is that promise written down, and until the matching settle the run will not end. That is the whole of Park, don't end.

An untracked deliverable is news. A detached shell job that exited, a watcher that noticed something — nothing acknowledged them, nothing is waiting for them, and a run that parked on one would park for ever because nothing has undertaken to produce it. push puts it in the queue and touches the outstanding count not at all: it will be delivered at the next round boundary if the run is still going, and it will not by itself keep the run alive.

quiet, and the race it closes

quiet is the finish decision, and the reason it is a method here rather than two reads at the call site is that it must be one snapshot:


    quiet  =  nothing outstanding  AND  nothing queued

Both halves, under one lock, at one instant. Read separately — "outstanding is 0" and then, a microsecond later, "the queue is empty" — they describe two different moments, and the operation that settled between them is a result that reaches nobody: the run ends, and the answer the model was told to expect is dropped on the floor.

state is the same snapshot with the numbers in it (< { outstanding, queued, quiet } >), and it is what the park polls: one lock acquisition per pass, and a decision that cannot be made from a world that has moved on.

Single consumer: drain is the driver's

drain is destructive: what it hands back is gone from the bus, because the bus has no way of taking it back if the caller drops it. That makes it exactly as safe as one consumer and no safer, and the contract is therefore explicit:

Only the run's driving thread may call drain, at a round boundary, and it must record everything it is handed. Everything else here — open, settle, push, quiet, state, outstanding-ops, close-all — is safe from any thread and may be called concurrently from as many as you like.

A second consumer does not corrupt anything: the lock is real. What it does is take half the completions and put them somewhere the model will never see them, which is a bug that looks like a flaky model rather than like a race.

The bus lock is a leaf

Nothing this class does calls anything it does not own: no callback, no emit, no I/O, no other lock. Every method is a short critical section over a Hash and an Array.

That is a promise callers may rely on, and it is what makes it safe to call open or push from inside somebody else's critical section — a composer's question table, a notification sink on a flusher thread. Taking the bus lock can never wait on anything except another bus operation, and no bus operation waits on anything at all.

The other side of the same promise: do not put callbacks on it. If this class ever grows an on-settle hook, the leaf property is gone and so is the freedom above.

A deliverable

A deliverable is plain data, and its shape is the contract between whoever produced the work and the loop that renders it:

Key What it is
kind REQUIRED. What sort of event this is: 'subagent-settled', 'job-exited'
head The framing and the identity — who, what, and that this is not the user
body The content, which the loop routes through the observation excerpt seam
tail What the model should do about it
extras Envelope extras for the injected turn: completion-of, call-id, ...

The three-way split is not decoration. The loop excerpts body and only body: a child that answered with a megabyte cannot be allowed into an unelidable user turn whole, and framing that got excerpted along with it would leave a turn that no longer says it is an automated event. So the words that must survive at any size go in head and tail, and the words that may be cut go in body.

A deliverable with no kind, or with nothing at all in any of the three text fields, is refused — settle closes the operation anyway and push answers False. An empty user turn is worse than a missing one.

seq (this bus's own arrival order) and op-id (the tracked operation's key, absent for an untracked deliverable) are stamped on the way in and are not the caller's to supply.

Scope: one bus per conversation

A bus belongs to a conversation, not to a run. A child that settles thirty seconds after its parent run was cancelled has still done the work and still has something to say, and the next run of the same conversation is exactly who should hear it — which is the same argument that makes a steer queue outlive one run.

clear is the other end of that: a host swapping to a different conversation calls it, and everything the old one had outstanding goes, because a completion delivered into a conversation that never asked for it is a turn out of nowhere.

SEE ALSO

LLM::Agent::Loop (the park, the round-top drain, and the only caller of drain), LLM::Agent::Subagents (the composer that opens and settles a delegation), LLM::Agent::Event (BackgroundOpStarted, BackgroundOpSettled, BackgroundOpDelivered, RunParked, RunResumed).

  • the bus is at max-outstanding (< reason => 'at-capacity' >);

  • $key is already open (< reason => 'duplicate' >) — which is a bug in the caller rather than a condition, but not one worth taking a tool call down over.

LLM::Agent v0.6.1

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.0+>:auth<zef:apogee>MCP::Client:ver<0.5.0+>:auth<zef:apogee>JSONL:ver<0.1.6+>:auth<zef:apogee>JSON::Fast:ver<0.19+>:auth<cpan:TIMOTIMO>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::Run
  • LLM::Agent::RunContext
  • LLM::Agent::Session
  • LLM::Agent::Subagents
  • LLM::Agent::TokenCount
  • LLM::Agent::ToolOperation

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.