# Agents (/docs/agents)



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](/docs/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](/docs/memory) for the per-agent fact
namespace that shares this scope, and [Plans](/docs/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).

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

<SourceMeta source="{ label: &#x22;src/agents/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/agents&#x22; }" />

## The composing-agents cluster [#the-composing-agents-cluster]

Agent profile is one of three concepts paired with
[Skill](/docs/skills) (the unit-of-work definition) and
[Subagent](/docs/subagents) (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](/docs/concept-clusters#composing-agents-cluster);
the rest of this page is the deep dive on the registry.

## `AgentProfile` shape [#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` [#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 [#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` [#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 [#example]

```typescript
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 [#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.

```typescript
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 [#rendering-a-profile-summary]

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

```typescript
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 [#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 [#where-to-go-next]

<Cards>
  <Card title="Subagents" href="/docs/subagents" description="The runtime spawn handle that writes invocation entries into a profile." />

  <Card title="Plans" href="/docs/plans" description="Plan steps carry a `suggestedAgent` that resolves against a profile's `specName`." />

  <Card title="Skills" href="/docs/skills" description="A skill's `name` is the spec name a profile keys against." />

  <Card title="Memory" href="/docs/memory" description="`LearningAuditor.detectConflicts` reads profile names to scan per-agent fact namespaces." />

  <Card title="Query" href="/docs/query" description="`getLearningHealth` and aggregate readers project profiles for review dashboards." />
</Cards>
