# @ai-matrx/agents

Portable client-side primitives for AI Matrx agent applications. The package
standardizes the stream wire, pure request/workflow projection, safe
Creator-facing result boundaries, and THE agent picker — without importing
Redux, Next.js, or application code.

**This package talks to the fully assembled AI Dream brain. It does not run the
provider/tool loop locally; use `@ai-matrx/agent-engine` when you need to run a
brain you assemble with your own providers, tools, persistence, and memory.**

## Install

```bash
pnpm add @ai-matrx/agents
```

## Read an agent stream

```ts
import { readMatrxNdjsonStream } from "@ai-matrx/api/stream";

for await (const envelope of readMatrxNdjsonStream(response.body!, {
  onMalformedLine: reportProtocolDamage,
  onUnknownEnvelope: reportUnknownEnvelope,
})) {
  handleEvent(envelope);
}
```

The reader preserves split UTF-8, drains the network independently of consumer
work up to the configurable `maxReadAhead` bound, supports cancellation,
normalizes full and compact Matrx envelopes, and reports malformed or unknown
input through explicit callbacks. `onValidEnvelope` observes the exact parsed
wire value before normalization without consuming it. Full-envelope
`stream_seq` is preserved for projector replay suppression.

## Frame non-ReadableStream transports

```ts
import { createMatrxNdjsonFramer } from "@ai-matrx/api/stream";

const framer = createMatrxNdjsonFramer({
  onMalformedLine: reportProtocolDamage,
  onValidEnvelope: persistRawEnvelope,
});

handleEvents(framer.pushBytes(extensionMessageBytes));
handleEvents(framer.pushText(desktopBridgeFragment));
handleEvents(framer.finish());
```

The same incremental framer works with browser-extension messages, desktop
bridges, WebSockets, and tests. `finish()` flushes split UTF-8 and diagnoses an
invalid unterminated final line with `atCompletion: true`.

## Present a settled result safely

```ts
import { projectAgentResultForDisplay } from "@ai-matrx/agents/presentation/result";

const displayValue = projectAgentResultForDisplay(persistedExecutionValue);
```

The projection removes provider-private reasoning blocks and signature material
without mutating the execution value. It is only for display, JSON views, and
exports. Never persist the projected result or use it to continue an agent run.

## Project workflow live output

```ts
import {
  createWorkflowNodeProjection,
  projectWorkflowNodeEvent,
} from "@ai-matrx/agents/projection/workflow";

let node = createWorkflowNodeProjection({ runId, nodeId });
node = projectWorkflowNodeEvent(node, nodeStreamFrame);
```

This is the single workflow presentation inlet. It keeps answer and private
reasoning separate, rejects replayed frames, assembles bounded server
`render_block` snapshots, and prevents shadowed text from being interpreted as
a second copy of the same content. Configure `maxOpenFrameSets`,
`maxFramesPerBlock`, and `maxBytesPerBlock` when creating the projection;
rejected or malformed blocks are observable through `lastRenderBlockIssue`.

## Run an agent end-to-end (`./matrx`)

The `@ai-matrx/api` subpath is the Matrx API client. Build the
PRODUCTION transport from identity alone — everything else (connect/total
timeout policy, fresh-per-call credential
headers, the fail-closed org header, error classification, diagnostics
hooks) ships in the package with the proven production defaults:

```ts
import {
  createMatrxTransport,
  newEphemeralConversationStart,
  runAgentToCompletion,
} from "@ai-matrx/api";

const transport = createMatrxTransport({
  baseUrl: "https://server.app.matrxserver.com",
  credentials: { get: async () => ({ kind: "api_key", apiKey }) }, // CredentialsPort (@ai-matrx/data)
  organizationId: orgId, // omit for conversation-lane calls (org rides the body)
});

const run = await runAgentToCompletion(transport, agentId, {
  ...newEphemeralConversationStart(),
  organization_id: orgId,
  user_input: "Summarize this quarter's numbers.",
}, { onChunk: (text) => render(text) });
```

Hosts with dynamic routing pass `resolveTarget` (fresh per call: base URL,
extra policy headers, channel label) instead of `baseUrl`, and can wire the
typed `diagnostics` seam (`onRequest` / `onError`)
into their telemetry. `MatrxTransport` remains a plain fetch-shaped port, so
a fully custom transport still plugs in.

Lower-level pieces: `startAgentRun` / `continueAgentConversation` /
`resumeAgentConversation` return a `MatrxRunHandle` (`X-Request-ID` and
`X-Conversation-ID` before any event, plus the normalized envelope stream);
`cancelAgentRun` stops a run by its server request id;
`getRuntimeOperationStatus` / `listRuntimeOperationEvents` /
`followRuntimeOperationEvents` / `rejoinRuntimeOperation` are the reconnect
ladder — and `followRuntimeOperationToEnd` is the full production reconnect
policy over them (stall watchdog, bounded retry budget, durable
`Last-Event-ID` cursor, all knobs typed and defaulted);
`submitAgentToolResults` + pending-call discovery drive delegated
client tools (watch `continuation_needed` → `resumeAgentConversation`).

## Run a job and render its result in five lines (`./intelligence`)

Any client — a plain Vite page included — runs any mandate, agent- or workflow-held, by key alone:

```tsx
import { createIntelligencePort } from "@ai-matrx/agents/intelligence";
import { useMandateRun } from "@ai-matrx/agents/intelligence/react";

const port = createIntelligencePort({ transport });
const { run, status, text, result, inputs, answer, error } = useMandateRun(port, "seo.audit");
// run({ organizationId, userInput }) -> status: resolving > running > needs_input > done
```

Core (no React): `port.run(...)` resolves a `RunRef` as soon as headers arrive; events ride
``port.events.on(`run:${ref.requestId}`, (e) => ...)`` (`text`, `status`, `needs_input`, `result`, `error`;
late subscribers get a replay). `describe`, `result`, `listRuns`, `answer`, `rejoin`, `createSoft` all return
`Result<T>` and never throw; an error's `message` is the server's own sentence. Keys are data: only
`<feature>.<name>` format is checked locally.

The transport: `createMatrxTransport({ baseUrl, credentials })` from `@ai-matrx/api`. Leave its
`organizationId` unset — the port sends each run's `organizationId` (and `createSoft`'s) as that call's
`X-Organization-Id`; `rejoin` and `answer` act in the organization the run was started in (read from its result —
never the page's current one; a guest run has none); `describe` / `result` / `listRuns` need none. A transport bound to an org getter that returns
null refuses every call before the wire (`organization_context_required`), reads included.

`result(ref)` is the durable read (a reload, another device, later). It is settled once the run's stream ends
(`status: "completed"` / `"waiting_input"` / an `error`); read at the moment of the `result` event it may still say
`running` while the run finishes — the `result` event itself already carries the envelope. When a mandate declares
an output kind but no envelope exists, `note` says why.

## React hooks (`./react`)

Client-only bindings over the same transport:

Runs start through `POST /ai/mandates/{key}` (`startMandateRun` / the IntelligencePort); there is no one-shot run hook.

`useFollowRuntimeOperation({ transport, executionId, onEvent })` is the
reconnect follower as a hook — production stall/retry/cursor policy built in.

## THE agent picker (`./catalog` + `./catalog/react`)

There is ONE agent picker on this platform and it ships here. An app provides a
Supabase client and who the user is; the picker's contract back is
`onSelect(agentId)`.

```ts
// once, anywhere
import { createAgentCatalog } from "@ai-matrx/agents/catalog";

export const agentCatalog = createAgentCatalog({
  client: supabase,                                   // REQUIRED
  identity: { requireUserId: () => requireUserId() }, // REQUIRED
  transport,   // only when a picker passes `defaultMandateKey`
});
```

```tsx
// once, at the top of your tree
import "@ai-matrx/agents/catalog/styles.css";
import { AgentCatalogProvider, AgentListDropdown } from "@ai-matrx/agents/catalog/react";

<AgentCatalogProvider catalog={agentCatalog} LinkComponent={Link}>
  <AgentListDropdown activeAgentId={agentId} onSelect={setAgentId} />
</AgentCatalogProvider>
```

You get, on every surface and in every app: the complete catalog read (owned
+ shared + org-shared + platform agents), Mine / Shared / All / Public tabs
with live counts, sort, favorites (the caller's own stars, read and written in
`platform.user_entity_state`), category and tag filters,
local relevance scoring plus server-side search, the current agent pinned on
top, per-row origin badges, a hover detail panel, a mobile drawer, and a footer
count. `AgentListInlinePicker` is the same core without the popover shell.

Register this package's build with your Tailwind source scan so the picker's
utilities are generated:

```css
@import "tailwindcss";
@source "../node_modules/@ai-matrx/agents/dist";
```

**The default row.** Pass `defaultMandateKey` and the picker offers the
platform default for that Mandate, resolved LIVE through the server and
labeled with the Holder's real name — no client ever hardcodes an agent name
again. Its id is `mandate:<key>`. If the cached name and the live one disagree,
the picker shows a persistent banner and calls your `notifier`.

**A host filter.** Pass `agentFilter={{ label: "Makes: Image", test }}` and
the picker lists only agents passing `test`, names the filter in a chip at the
top of the list with "Show all", and keeps the current agent pinned whether or
not it matches. Give `onClear` to own the clear yourself; without it, "Show
all" sets the filter aside until the dropdown next opens. Never a silent filter.

**Ports.** `client` and `identity` are required and throw when absent. Every
other port ships a working default: `errorSink` (tagged console), `notifier`
(the banner is the primary channel), `storage` (localStorage, else memory),
`favorites` (the `@ai-matrx/associations` `ues_*` chokepoint built on your
client — pass your own instance if you hold one),
`labels`, `navigate` (`window.location.assign`), `LinkComponent` (a plain
`<a>`), `renderModelRef` (plain text) and `openPeek` (absent → the affordance
is hidden, never dead).

`assertAgentCatalogSchema(client)` proves at boot that the database can answer
`agx_get_list_full`, `agx_search`, `agx_resolve_agent_address` and
`ues_get_bulk` (the star read).

## The one Source input (`./sources`, `./sources/runtime`, `./sources/react`)

`./sources` is the wire contract (`SourceRef`, `SourceSet`, `SourceManifest`,
`ResolvedSourceSet`). `./sources/runtime` is the UI-free core every screen of
the Source input sits on — pure TypeScript, with every outside dependency
injected:

```ts
import {
  createSourceSetController,
  createSourceIntake,
  createSourcesClient,
  createFetchSourcesTransport,
  createMemorySourceSetStore,
  planSourceReview,
} from "@ai-matrx/agents/sources/runtime";

const client = createSourcesClient(
  createFetchSourcesTransport({ baseUrl, headers: async () => ({ Authorization: `Bearer ${token}` }) }),
);
const set = createSourceSetController({
  store: createMemorySourceSetStore(), // or your own store adapter
  client,
  config: { defaultForm: "clean", deliveries: ["direct"], max: 5 },
});
const intake = createSourceIntake(set, { doors, ensureOrganization, /* … */ });
await intake.addPastedText(text, "Chapter 8 notes");
await set.manifest(); // sizes, forms, parts on each card
const plan = planSourceReview({ manifest, refs, base: set.toSourceSet(), modelWindowTokens, fallbackWindowTokens });
const grounded = await set.resolve();
```

`./sources/react` binds it: `useSourceSet(adapter)`, `useSourceIntake(set, deps)`,
`useSourceRecovery(set, intake, runner, options)`, `useSourcePartsSearch(client, ref, query)`.

## Context value rules (`./context`, `./context/react`)

Per value, a person controls two knobs: whether it reaches the model (`include`) and the size up
to which it is inlined (`max_inline_chars`; above it the agent reads it on request).

```ts
import { resolveContextRow, buildContextWire, compareReceipt } from "@ai-matrx/agents/context";

const rows = values.map((v) => resolveContextRow(v, savedRules, cap)); // what the table shows
const context = buildContextWire(rows);                               // what the request sends
// on the `context_receipt` event:
const { mismatches, systemRows } = compareReceipt(rows, receipt);
```

`./context/react` renders them: `ContextRulesTable`, `ContextRulesChip`, `ContextRulesPanelBody`
(presentational; the host persists every `onChange`).

## Runtime support

The package targets modern browsers, browser-based desktop shells,
extensions, Next.js client or server modules, and Node 20+. Its one runtime
dependencies are `@ai-matrx/data` (the shared net-resilience + credentials
layer), `@ai-matrx/design-system` (the picker's primitives) and
`@ai-matrx/associations` (the per-user star chokepoint); `react >= 18`
is an optional peer needed only by `./react`, `./sources/react`, `./catalog/react` and `./context/react`. No entry
point performs work at import time.

## Development

```bash
pnpm typecheck
pnpm test
pnpm check:package
```

`check:package` builds JavaScript and declarations, validates the manifest,
packs the release artifact, checks its public types, installs it into an empty
project, and imports every entry point.

Never publish directly from this working directory. Release the verified
tarball produced by `pnpm pack`; `prepublishOnly` blocks the unsafe path.

## THE model picker (`./models` + `./models/react`)

```tsx
import { createModelCatalog, createModelFavorites } from "@ai-matrx/agents/models";
import { ModelCatalogProvider, ModelListDropdown } from "@ai-matrx/agents/models/react";

const catalog = createModelCatalog({ client: supabase });
const favorites = createModelFavorites({ client: supabase });
// <ModelCatalogProvider catalog={catalog} favorites={favorites}>
//   <ModelListDropdown modelOnly value={modelId} inputModalities={["text"]} onValueChange={setModelId} />
// </ModelCatalogProvider>
```

No Redux, no chat. Optional provider ports: `isSuperAdmin`, `hiddenModelIds`, `LinkComponent`, `settingsHref`,
`renderErrorAction`. Favorites paint from an injectable cache (default in-memory) and persist through the
canonical `ues_set` ledger.

## THE skill catalog (`./skills` + `./skills/react`)

```ts
import { createSkillCatalog } from "@ai-matrx/agents/skills";
import { useSkills } from "@ai-matrx/agents/skills/react";

const catalog = createSkillCatalog({
  client: supabase, // a Supabase client
  associations: { skillIdsForProject, projectIdsBySkill }, // skill ↔ project membership
});
// reads only: catalog.fetchSkills / fetchSkillById / fetchSkillCategories / applyStreamEvent
// in a component: const { skills, loading } = useSkills(catalog);
```

The skill writes (create, patch, delete, categories, resources) stay with the host's editors, which report what landed through the local-only `apply*` methods.
