NAME

App::karr::Foundation::Agents - karr-foundation agent definitions, invocation contract and availability

VERSION

version 0.600

DESCRIPTION

App::karr::Foundation::Agents owns the named agent definitions from karr-foundation's local config and the availability record kept for each of them.

A definition says what to run (command), under which invocation contract (kind), how often to retry it once it has stopped working (probe_every), roughly how many of it may run side by side (concurrent, the operator's estimate -- see App::karr::Foundation::Limits) and, in prose, what it is good at (description). karr never reads the description; it is carried for the coordination agent that routes work, because the thing choosing is a language model and prose is what it reads best.

Availability is the least karr can know: ok, or failing since a moment with a next attempt due at another. No cost, no tokens, no quotas -- a rate limit and an exhausted budget look identical from the outside (the command stops working), so one mechanism covers both. Where the reset rhythm is known it is configured as probe_every; where it is not, the agent is retried at a fixed interval and every recovery is recorded, so a pattern can be read out of the record later. Reading that pattern is the coordination agent's job, never a learning algorithm in here.

That record is shared by every board on the machine and several of them run at once (#186), so a read-modify-write of it is serialised through an flock(2) on a sibling agents.state.lock. Losing one of two overlapping updates is cheap in one direction and not in the other: a lost "it is failing" costs one more attempt, a lost "it works again" parks every board on that agent for another probe interval.

foundation

The owning App::karr::Foundation instance, held weak_ref to avoid a reference cycle (Agents lives inside the Foundation it belongs to). Required. Definitions and the default probe interval are read from its _config_data; the availability state file sits beside its _config_path.

definitions

Every configured agent definition, keyed by name, lazily parsed and validated from "foundation"'s agents: config section. Each value is a hashref carrying at least command and kind (defaulted to shell), plus whatever else the config gave it -- probe_seconds is added during parsing from probe_every (see "probe_seconds"), and role (only coordinator today) marks the one agent that is the fleet's judgement layer (App::karr::Foundation::Coordinator). An empty hashref when the config has no agents: section at all; a malformed section or definition (not a mapping, no command, an unknown kind) dies rather than starting with a fleet that silently cannot run.

definition

my $def = $agents->definition('claude-code');

The parsed definition hashref for $name (see "definitions"), or dies naming the known agents -- or saying the config has none at all -- rather than handing back undef for a typo'd name.

names

my @names = $agents->names;

Every configured agent name, sorted.

invocation

my $inv = $agents->invocation('claude-code');
# { name => 'claude-code', kind => 'claude-code',
#   command => '...', render => 'stream-json', description => '...' }

Everything App::karr::Foundation::Runner needs to run one named agent: the assembled shell command, the kind and name to log it under, a render hint (stream-json for kind: claude-code, undef otherwise), and the definition's description. For kind: shell the command is the config's command template verbatim; for kind: claude-code it is built by appending karr's own arguments -- --output-format stream-json --verbose (so the run streams live output and ends with the structured result App::karr::Foundation::Runner reads), --permission-mode, --max-turns, and --allowed-tools as configured.

state_file

The path to agents.state, the machine-local, agent-scoped availability record -- lazily built as a sibling of "foundation"'s _config_path, so it follows --config the way the config itself does.

It lives neither in .karr.state (per-repository, and availability is not a property of a repository -- two boards driven by the same agent must share one answer, or the second rediscovers the outage on its own) nor on the board (refs/karr/config syncs, and an agent command or a spent account limit is local to this machine, not something to push at a colleague's fleet). Config-adjacent and unsynced is the one place both constraints allow.

availability

my $rec = $agents->availability('minimax');
# { state => 'ok' }
# { state => 'failing', failing_since => EPOCH, next_attempt => EPOCH, ... }

What is known about $name right now, as one of the two states the spec names -- ok, or failing with failing_since, next_attempt, and (when one was given) last_error. Timestamps are epoch seconds, as .karr.state's cooldown_until are; rendering them for a human is karr-foundation --status's job, not this method's.

available

next unless $agents->available('minimax');

May $name be run right now? True for an agent that is ok, and true again for a failing one whose "probe_seconds" has elapsed since failing_since -- that elapsed check IS the probe. There is no separate probing run: retrying the agent on the work already waiting is the probe, and a probe that skipped the work would be a second kind of run to reason about.

probe_seconds

my $wait = $agents->probe_seconds('minimax');

How long a failing $name waits before it is retried: the agent's own probe_every when its definition set one, otherwise "default_probe_seconds".

default_probe_seconds

The fallback probe interval for an agent whose definition sets no probe_every of its own: lazily read from the config's top-level probe_every, or 600 (ten minutes) when that is absent too -- short enough that a five-minute blip does not park a fleet for an hour, long enough that a hard rate limit is not hammered once a minute.

record_failure

$agents->record_failure('minimax', 'rate limited');

Records that $name just failed and schedules its next probe "probe_seconds" out. Which of the many reasons it can fail is not asked -- a rate limit, a spent budget, a revoked key, and a wrapper missing on this machine all present the same way (it does not work now), and the same fixed-interval retry is the right answer to all of them. $reason (optional) is kept verbatim as last_error, for an operator or --status to read, not for anything to branch on. failing_since is set once, on the first failure of a run, and survives every further one, so it keeps saying when the outage started rather than resetting on each attempt.

record_success

$agents->record_success('minimax');

Records that $name worked. A no-op, both for the caller (returns 0) and for the state file (nothing is written), when the agent was already ok -- an ordinary successful run would otherwise rewrite the file for no news. For one that was failing it moves the record to ok and appends a recovery entry (failing_since, recovered_at, seconds, and the last last_error if there was one) to a capped history, and returns 1. karr does not learn a reset rhythm from that history; it leaves a legible trail of outages for whoever -- a person, the coordination agent -- wants to read one out of it.

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/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 <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)