/** * `@ai-matrx/agents/catalog` — the row shape and the per-consumer view state. * * `AgentSummary` is the 19-column row `agx_get_list_full()` returns, camelCased. * It is deliberately NOT the editable agent record: dirty tracking, undo, * messages, tools and `_fetchStatus` stay in whichever host owns the editor. * A list row is a list row. * * The view-state types are a VERBATIM port of matrx-frontend * `features/agents/redux/agent-consumers/slice.ts` — only the coupling seam * inverts (Redux reducers become store actions in `./store`). */ /** * A branded agent id. Every id that reaches `onSelect` is one of these: * either a `agent.definition` UUID, or the `mandate:` reference a * default row carries (byte-identical to the ref shape matrx-extend * `src/lib/mandates.ts` and matrx-local `desktop/src/lib/mandates.ts` * already hand back to their hosts). * * 🚨 The brand is a STRING LITERAL, never `unique symbol`. This package emits * `./catalog` and `./catalog/react` as two separate bundles, so each `.d.ts` * carries its own copy of these declarations. A `unique symbol` brand is * NOMINAL: two copies of it are unrelated types, so `createAgentCatalog()` * from `./catalog` was not assignable to `` * from `./catalog/react` — the exact two-import wiring the README, the * CHANGELOG and every consumer use. A literal brand is structural, so the two * copies unify, while a bare `string` still cannot be passed where an * `AgentId` is required (which is the whole point of branding). Any future * brand in this package follows the same rule. */ type AgentId = string & { readonly __agentId: "ai-matrx.agent-id"; }; /** `agent_type` verbatim. Only `builtin` changes how a row is grouped. */ type AgentCatalogAgentType = "user" | "builtin"; /** * `access_level` verbatim from `agx_get_list_full`. Widened over the raw * string because it is a DB-owned vocabulary: an unknown level renders as * itself rather than being coerced into a wrong one. */ type AgentAccessLevel = "owner" | "admin" | "editor" | "viewer" | "system" | (string & {}); /** * Present on a catalog row ONLY when that agent is an **Orchestra** — a * conductor that delegates to member agents at run time. Choosing one for a * step, a chat or a workflow node runs the whole ensemble, so a person has to * be able to SEE that before choosing, not discover it from the run. * * The DB decides. `agx_get_list_full` / `agx_get_list` / `agx_search` build * this from `platform.associations` (the conductor self-edge plus its member * edges) in one `agx_orchestra_badges()` join — never a second fetch, and * never a per-row read. `null` for every ordinary agent. * * History: the badge lived only in the Python `GET /agents` route, so every * client lost it when the pickers moved to the RPC. It is a ROW FIELD now, in * the one read every client shares. */ interface OrchestraBadge { /** How the ensemble runs: `supervisor` | `sequential` | `parallel` | `dag`, or a future mode verbatim. */ mode: string; /** The composition's one-line description, when its author wrote one. */ tagline: string | null; /** * The per-composition delegation-depth budget AS DECLARED, or `null` when * the composition declares none. Deliberately NOT defaulted here: the * platform's fallback ceiling is enforced by the server that runs the * ensemble, and a badge that invented a number would be reporting a promise * nobody made. */ depthBudget: number | null; /** Member edges. From the ROW count, so an unlabelled ensemble still counts. */ memberCount: number; /** Only REAL role titles — an ensemble with unlabelled edges yields `[]`. */ memberTitles: string[]; } /** * One `agx_get_list_full()` row, camelCased. Nineteen columns, no more: * anything richer needs a different read and is a different type. */ interface AgentSummary { id: AgentId; name: string | null; description: string | null; category: string | null; tags: string[]; agentType: AgentCatalogAgentType; modelId: string | null; /** * The agent's CLASS pin (`settings.offering_id` — which serving class of * the model runs, e.g. "Matrx Lightning"). `null` = no pin (the model's * preferred class runs). ABSENT = the read did not carry the column, so the * class is unknown and a display must name the model alone. */ offeringId?: string | null; isActive: boolean; isArchived: boolean; isFavorite: boolean; createdBy: string | null; organizationId: string | null; /** * The owning organization's NAME, joined by the DB in the same read as the * row (`organization_name` on `agx_get_list_full` / `agx_search`) — never a * second per-row query. Two agents can share a name across organizations; * this is what tells them apart. `null` = no organization; ABSENT = the read * did not carry the column (a database that predates it). */ organizationName?: string | null; taskId: string | null; sourceAgentId: string | null; createdAt: string | null; updatedAt: string | null; isOwner: boolean | null; accessLevel: AgentAccessLevel | null; sharedByEmail: string | null; /** * The Orchestra badge, or `null` for an ordinary agent. Strict: every row * carries the key, so a surface reads `agent.orchestra` and never has to ask * whether the column existed. */ orchestra: OrchestraBadge | null; /** * Set only on a mandate default row — the key it resolved through, so a * host can tell "the default" from a stored agent without parsing the id. */ mandateKey?: string; /** * THE THIN ROW (FW-014). `true` when this row came from the picker's page * read (`agent.agx_picker_page`), which carries only what a picker row * renders: no description, model, offering, task, source or sharer. A * consumer that needs those asks `catalog.ensureAgentRows([id])` (hover, * open), which replaces it with the full row. Absent = a full row. */ isThin?: boolean; /** When the caller last used this agent (their latest conversation with it). Page reads only. */ lastUsedAt?: string | null; } type AgentSortOption = "updated-desc" | "created-desc" | "name-asc" | "name-desc" | "category-asc"; /** Which ownership tab is active in the agent list. */ type AgentTab = "mine" | "shared" | "all" | "system"; /** Favorite filter. */ type AgentFavFilter = "all" | "yes" | "no"; /** * The archive filter — THE ARCHIVED-ITEMS LAW's ONE control, in this * package's existing vocabulary. * * > "everything should have an archive filter, and the default should always * > hide archived, but seeing archived items should be one or two clicks * > away." — Arman, 2026-09-09 (`common-docs/policies/archived-items.md`) * * Three states and no more, identical for every entity on the platform: * * | value | the platform's words | what the list shows | * |------------|----------------------|--------------------------| * | `active` | hide archived | only un-archived rows | * | `both` | show all | every row | * | `archived` | archived only | only archived rows | * * `active` is the platform default, and it is a KNOB — see * `AgentCatalogConfig.defaults.archiveFilter`. */ type AgentArchFilter = "active" | "archived" | "both"; /** * Access level filter. * 'any' = no restriction (default). * 'owned' = only agents the user owns (isOwner = true). * 'shared' = only agents shared with the user (isOwner = false). * 'editable' = owner + admin + editor. */ type AgentAccessFilter = "any" | "owned" | "shared" | "editable"; interface AgentConsumerState { tab: AgentTab; sortBy: AgentSortOption; searchTerm: string; /** INCLUSION model: empty = show all; non-empty = only matching. */ includedCats: string[]; /** INCLUSION model: empty = show all; non-empty = only matching. */ includedTags: string[]; favFilter: AgentFavFilter; archFilter: AgentArchFilter; accessFilter: AgentAccessFilter; favoritesFirst: boolean; /** Current page for owned-agent list items (after the card section). */ listPage: number; /** Current page for shared-agent list items. */ sharedPage: number; /** * Ids returned by the last server-side search (`agx_search`), in server rank * order. Additive: an agent in this list survives the search filter even * when the local scorer gives it 0. * * That is load-bearing for tier 2. A deep search matches an agent's prompt * content, which the client never loads — so the local scorer cannot see it. * Without this list the server would return prompt matches and the UI would * immediately filter them back out. * * Server-only matches sort BELOW every locally-scored match, in server rank * order, so obvious matches always come first. */ serverMatchedIds: string[]; /** True while a server search is in flight — drives the search spinner. */ isServerSearching: boolean; /** Tier 2: also search agent prompt content. Opt-in, per consumer. */ deepSearch: boolean; } /** The subset of consumer state a caller may patch (paging is derived). */ type AgentConsumerPatch = Partial>; export type { AgentSummary as A, OrchestraBadge as O, AgentConsumerState as a, AgentConsumerPatch as b, AgentTab as c, AgentSortOption as d, AgentFavFilter as e, AgentArchFilter as f, AgentAccessFilter as g };