pleach
Build

Agents

AgentRegistry — persistent per-spec profiles with a rolling 100-invocation window, derived stats, and a trend signal.

An agent profile is the variable surface the registry maintains per spec name: invocation count, the last 100 invocations as a rolling window, per-tool success and failure counts, intent affinity, and a first-half-versus-second-half trend on success rate. The substrate doesn't pick which agent runs — that's the orchestrator's job — but it owns the longitudinal record of how each spec has performed for this user.

Distinct from subagents: subagents are runtime spawn handles (@pleach/core/subagents); agent profiles are persistent registry records (AgentRegistry). A subagent invocation writes into the profile of its spec; the profile outlives any individual spawn. See Memory for the per-agent fact namespace that shares this scope, and Plans for how a step's suggestedAgent resolves against a profile's specName.

AgentRegistry is re-exported from the package root. The underlying types live under the real @pleach/core/agents/types subpath (an explicit, emitted export).

import { AgentRegistry } from "@pleach/core";
import type {
  AgentProfile,
  AgentInvocationEntry,
  AgentDerivedStats,
  ToolUsageStat,
} from "@pleach/core/agents/types";

The composing-agents cluster

Agent profile is one of three concepts paired with Skill (the unit-of-work definition) and Subagent (the child runtime that executes one). The cluster sits between the orchestrator and the audit ledger — it names the unit, spawns the runtime, and records the rolling signal that decides whether to spawn it again. The full triplet framing lives at Concept clusters → Composing-agents; the rest of this page is the deep dive on the registry.

AgentProfile shape

FieldTypeNotes
specNamestringThe spec this profile is for
orgIdstringOrganization scope
userIdstringUser scope, or "__org__" for org-wide
firstSeenISO-8601Created lazily on first recordInvocation
lastSeenISO-8601Bumped on every recorded invocation
invocationsnumberTotal count, never decays
performanceobject{ windowSize, entries } — rolling window
toolUsageRecord<string, ToolUsageStat>Per-tool counts
intentAffinityRecord<string, number>Intent frequency
notesstring | undefinedUser-provided annotation

The rolling window holds the last 100 invocations (MAX_WINDOW = 100). Older entries fall off; invocations keeps the lifetime count.

AgentInvocationEntry

FieldTypeNotes
sessionIdstringWhich session produced the invocation
chatIdstringWhich chat
timestampISO-8601When the invocation completed
durationnumberms
statusenumsuccess / error / timeout / cancelled
toolCallsnumberTotal tool calls during the invocation
toolErrorsnumberSubset that errored
tokenUsagenumberTotal tokens
intentstring | undefinedTriggering intent classifier label
qualityScorenumber | undefined0–1, if the eval harness scored the run
toolsUsedstring[]Tool names called
errorSummarystring | undefinedShort error message on failure

AgentRegistry methods

MethodReturnsEffect
getProfile(specName)AgentProfileReads or constructs an empty profile; does not persist
recordInvocation(specName, entry)voidRe-reads the profile, appends, trims to 100, persists
listProfiles()AgentProfile[]Up to 100
getStats(profile)AgentDerivedStatsPure projection over the rolling window
formatProfileSummary(profile)stringSingle-line summary for prompt injection — invocations, success rate, trend arrow

recordInvocation re-reads the stored profile before mutating to narrow the lost-update window when concurrent subagents complete near-simultaneously. The registry doesn't ship a conditional-update primitive; if you need stronger guarantees, funnel writes through a per-specName lock at the caller.

AgentDerivedStats

FieldTypeNotes
successRatenumbersuccess count / window size
avgDurationnumberms, mean over the window
avgToolCallsnumberMean over the window
avgQualitynumberMean over entries with qualityScore; 0 if none
errorRatenumberFraction of entries with toolErrors > 0
trendnumbersecondHalf.successRate − firstHalf.successRate
sampleSizenumberWindow entry count
topToolsarrayTop 10 by call count
topIntentsarrayTop 5 by frequency

trend is the load-bearing signal — positive means the agent is improving over the window, negative means degrading. formatProfileSummary renders the trend as , , or with a ±2% deadband.

Example

import { AgentRegistry } from "@pleach/core";
import { MyStore } from "./my-store";

const registry = new AgentRegistry(new MyStore(), orgId, userId);

await registry.recordInvocation("research-fanout", {
  sessionId,
  chatId,
  timestamp:  new Date().toISOString(),
  duration:   8_240,
  status:     "success",
  toolCalls:  6,
  toolErrors: 0,
  tokenUsage: 12_400,
  intent:     "literature_search",
  toolsUsed:  ["search_corpus", "search_news", "fetch_url"],
});

const profile = await registry.getProfile("research-fanout");
const stats   = registry.getStats(profile);

if (stats.sampleSize >= 10 && stats.successRate < 0.6) {
  log.warn("research-fanout success rate below threshold", stats);
}

Recording a failed run

The entry below shows what to write when an invocation errors — errorSummary is the short string the UI surfaces, toolErrors counts the underlying tool failures.

await registry.recordInvocation("research-fanout", {
  sessionId,
  chatId,
  timestamp:    new Date().toISOString(),
  duration:     2_180,
  status:       "error",
  toolCalls:    3,
  toolErrors:   2,
  tokenUsage:   1_900,
  toolsUsed:    ["search_corpus", "fetch_document"],
  errorSummary: "fetch_document timed out twice",
});

The entry lands in the rolling window the same way a success does; the next getStats(profile) call reflects the new errorRate and the recomputed trend.

Rendering a profile summary

The snippet pulls the single-line summary formatProfileSummary returns and shows the trend arrow it bakes in.

const profile = await registry.getProfile("research-fanout");
const line    = registry.formatProfileSummary(profile);

// Example output:
// Agent "research-fanout" — 47 prior invocations, 81% success rate,
// ↓ 12% trend. Top tools: search_corpus, fetch_document, summarize.
console.log(line);

The arrow is / / with a ±2% deadband against stats.trend. An empty rolling window returns the empty string, so the line is safe to concatenate into a system prompt without a guard.

Persistence layout

PathHolds
[orgId, userId, "agents", "profiles"]AgentProfile, keyed by specName
[orgId, userId, "agents", <specName>, "facts"]Per-agent learned facts (see @pleach/core/memory)

The shared agents/<specName>/ prefix is what lets the LearningAuditor correlate a fact's per-agent scope with the profile that owns it — detectConflicts walks every profile in agents/profiles and reads facts out of agents/<specName>/facts to find disagreements.

Where to go next

On this page