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
| Field | Type | Notes |
|---|---|---|
specName | string | The spec this profile is for |
orgId | string | Organization scope |
userId | string | User scope, or "__org__" for org-wide |
firstSeen | ISO-8601 | Created lazily on first recordInvocation |
lastSeen | ISO-8601 | Bumped on every recorded invocation |
invocations | number | Total count, never decays |
performance | object | { windowSize, entries } — rolling window |
toolUsage | Record<string, ToolUsageStat> | Per-tool counts |
intentAffinity | Record<string, number> | Intent frequency |
notes | string | undefined | User-provided annotation |
The rolling window holds the last 100 invocations
(MAX_WINDOW = 100). Older entries fall off; invocations
keeps the lifetime count.
AgentInvocationEntry
| Field | Type | Notes |
|---|---|---|
sessionId | string | Which session produced the invocation |
chatId | string | Which chat |
timestamp | ISO-8601 | When the invocation completed |
duration | number | ms |
status | enum | success / error / timeout / cancelled |
toolCalls | number | Total tool calls during the invocation |
toolErrors | number | Subset that errored |
tokenUsage | number | Total tokens |
intent | string | undefined | Triggering intent classifier label |
qualityScore | number | undefined | 0–1, if the eval harness scored the run |
toolsUsed | string[] | Tool names called |
errorSummary | string | undefined | Short error message on failure |
AgentRegistry methods
| Method | Returns | Effect |
|---|---|---|
getProfile(specName) | AgentProfile | Reads or constructs an empty profile; does not persist |
recordInvocation(specName, entry) | void | Re-reads the profile, appends, trims to 100, persists |
listProfiles() | AgentProfile[] | Up to 100 |
getStats(profile) | AgentDerivedStats | Pure projection over the rolling window |
formatProfileSummary(profile) | string | Single-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
| Field | Type | Notes |
|---|---|---|
successRate | number | success count / window size |
avgDuration | number | ms, mean over the window |
avgToolCalls | number | Mean over the window |
avgQuality | number | Mean over entries with qualityScore; 0 if none |
errorRate | number | Fraction of entries with toolErrors > 0 |
trend | number | secondHalf.successRate − firstHalf.successRate |
sampleSize | number | Window entry count |
topTools | array | Top 10 by call count |
topIntents | array | Top 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
| Path | Holds |
|---|---|
[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
Subagents
The runtime spawn handle that writes invocation entries into a profile.
Plans
Plan steps carry a `suggestedAgent` that resolves against a profile's `specName`.
Skills
A skill's `name` is the spec name a profile keys against.
Memory
`LearningAuditor.detectConflicts` reads profile names to scan per-agent fact namespaces.
Query
`getLearningHealth` and aggregate readers project profiles for review dashboards.
Agent shapes
Reference agent designs Pleach is built for — each pairs a recurring production pattern with the runtime primitive that handles it and the team shape it shows up in.
Subagents
Spawn child runtimes from a tool call. Every child call lands on the ledger with parent_turn_id and subagent_depth — fan-out rolls back to the parent turn.