@ai-matrx/agents 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,230 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-09-08
4
+
5
+ **THE ONE PICKER NOW RENDERS ON REACT NATIVE — `@ai-matrx/agents/catalog/native`.**
6
+
7
+ Matrx Mobile (Expo 54 / RN 0.81) was the last client still hand-rolling its
8
+ agent list. Its sheet showed four HARDCODED agents plus a Supabase read of a
9
+ `prompts` table that **does not exist in the platform database** — a read whose
10
+ PostgREST error was swallowed into an empty array, so the list silently never
11
+ had anything but the four constants. Three of those four constants named the
12
+ WRONG agent: the ids behind "General Chat", "Deep Research" and "Code Helper"
13
+ resolve in `agent.definition` to *Agent Ledger*, *Knowledge Search Test* and
14
+ *Balanced News Analysis*. A label lying on screen, with nothing anywhere that
15
+ could notice — the same class the mandate default row was built to end.
16
+
17
+ React Native could not adopt the picker before this release for two concrete
18
+ reasons, and both are fixed here rather than worked around in the app.
19
+
20
+ ### Consumer action (C28)
21
+
22
+ **Web hosts: none.** `@ai-matrx/agents/catalog` and `/catalog/react` keep every
23
+ export, every name and every behaviour. The provider, the store subscription
24
+ and the two list hooks moved to `catalog/shared/*` so both platforms run one
25
+ implementation; `catalog/react` re-exports all of them unchanged, and the
26
+ `"use client"` banner is still stamped on the react build and still absent from
27
+ the pure ones (the tarball canary now judges eight artefacts instead of four).
28
+
29
+ **React Native hosts:**
30
+
31
+ ```tsx
32
+ import AsyncStorage from "@react-native-async-storage/async-storage";
33
+ import { createAgentCatalog } from "@ai-matrx/agents/catalog";
34
+ import { AgentCatalogProvider, AgentListSheet } from "@ai-matrx/agents/catalog/native";
35
+
36
+ const catalog = createAgentCatalog({
37
+ client: supabase,
38
+ identity: { requireUserId: () => requireUserId() },
39
+ storage: AsyncStorage, // NEW — the async storage seam
40
+ transport, // only when a picker passes defaultMandateKey
41
+ errorSink: reportToDiagnostics,
42
+ });
43
+
44
+ <AgentCatalogProvider catalog={catalog} navigate={(href) => router.push(href)}>
45
+ <AgentListSheet consumerId="chat" onSelect={setAgentId} activeAgentId={agentId} />
46
+ </AgentCatalogProvider>
47
+ ```
48
+
49
+ - `react-native` and `react-native-svg` are OPTIONAL peers. Install
50
+ `react-native-svg` (`npx expo install react-native-svg`) for the real glyphs;
51
+ without it the icons fall back to text glyphs and **say so once** through the
52
+ `errorSink`, with the remedy. Nothing goes blank and nothing goes quiet.
53
+ - Bind `navigate` if your rows should be able to open an agent. Unbound, the
54
+ default is a no-op that REPORTS through the `errorSink` — never a control
55
+ that silently does nothing.
56
+ - `@ai-matrx/agents/catalog/styles.css` is a WEB artefact; the native entry
57
+ needs no stylesheet.
58
+
59
+ ### What is new
60
+
61
+ - **`@ai-matrx/agents/catalog/native`** — the picker in React Native
62
+ primitives over the SAME headless kernel and the SAME hooks:
63
+ `AgentCatalogProvider`, `AgentListSheet` (the body of a bottom sheet — the
64
+ host keeps its own sheet chrome, so no gesture library is forced on anyone),
65
+ `AgentListInlinePicker`, and every part beneath them (`AgentListContent`,
66
+ `AgentListTabs`, `AgentFilterBar`, `AgentRow`, `AgentDetailCard`,
67
+ `AgentSubView`, `FavoriteAgentButton`, the five primitives, `AgentIcon`).
68
+ The shells take the web shells' prop surface name for name: `onSelect`,
69
+ `activeAgentId`, `consumerId`, `initialTab`, `includeSystemInAll`,
70
+ `visibleTabs`, `systemTabLabel`, `showPinnedAgent`, `excludeAgentIds`,
71
+ `defaultMandateKey`, `compact`.
72
+ - **The async storage seam** — `AgentCatalogAsyncStorage` /
73
+ `AgentCatalogStorageLike`, plus `readCachedDefaultRowAsync` and
74
+ `writeCachedDefaultRowAsync`. `createAgentCatalog({ storage })` now accepts an
75
+ `AsyncStorage`-shaped store verbatim. React Native has no `localStorage`, so
76
+ before this a native host could only hand the catalog a store that answered
77
+ `null` synchronously (a lie) or lose the default row's first paint entirely.
78
+ **The web path is unchanged**: `AgentCatalogStorage` is still synchronous,
79
+ `readCachedDefaultRow`/`writeCachedDefaultRow` still have their synchronous
80
+ signatures, `defaultStorage()` still prefers `localStorage`, and the async
81
+ helpers over a synchronous store produce byte-identical cache payloads
82
+ (proven in `catalog/__tests__/storage-seam.test.ts`).
83
+ - **`AgentCatalogNativeTheme`** (C26 on a platform with no CSS) — the native
84
+ twin of the `--matrx-agent-*` sheet: every token ships a DEFAULT VALUE so the
85
+ picker renders correctly unthemed, and a host re-brands by passing `theme` to
86
+ the provider. `DEFAULT_AGENT_CATALOG_NATIVE_THEME` (dark) and
87
+ `LIGHT_AGENT_CATALOG_NATIVE_THEME` ship; `resolveAgentCatalogNativeTheme`
88
+ merges an override and keeps every value it omits. No native component
89
+ contains a colour literal.
90
+ - **`catalog/shared/icon-paths.ts`** — the glyph geometry, once. The web icons
91
+ now read it too, so a shape can never differ between the two platforms; the
92
+ native entry draws the same paths through `react-native-svg` (C19: still no
93
+ icon-library dependency, on either platform).
94
+ - **`AgentListTabCounts` moved** to `catalog/shared/useAgentListCore` beside the
95
+ code that computes it. `catalog/react/AgentListTabs` re-exports it unchanged.
96
+
97
+ ### Behavioural deltas on native, recorded rather than hidden
98
+
99
+ 1. **No hover preview.** The web picker's side panel opens on hover; a touch
100
+ screen has no hover, so the detail card is reached by the row's chevron —
101
+ the same three-view stack (`list → detail → sort/categories/tags`) the web
102
+ `AgentListInlinePicker` already used on mobile widths.
103
+ 2. **No cmd-click link.** `AgentRow`'s anchor exists on the web so a person can
104
+ open an agent in a new tab. React Native has neither, so the row is an
105
+ ordinary `Pressable`; the `LinkComponent` port still exists and still ships
106
+ a working native default.
107
+ 3. **`autoFocusSearch` defaults to `false`** (the web inline picker defaults to
108
+ `true`): a keyboard that opens with the sheet covers the list it was meant
109
+ to filter.
110
+
111
+ ### Proof
112
+
113
+ - **The 293-case ordering matrix, re-measured through the RENDERED native
114
+ list** (`catalog/native/__tests__/parity.test.tsx`): every case's ids, in
115
+ order, read back out of the mounted native tree — not out of the selectors a
116
+ second time. Sharing a kernel is an architecture claim; this is the
117
+ measurement. It carries a falsifiability leg.
118
+ - **The tarball canary imports AND requires `./catalog/native`**, checks every
119
+ documented export, proves the theme override merges over the PACKED build,
120
+ proves the icon degradation announces exactly once with its remedy, and
121
+ compiles a native usage (including an `AsyncStorage`-shaped `storage`) against
122
+ the packed types under `exactOptionalPropertyTypes` + `noUncheckedIndexedAccess`.
123
+ - **The banner canary now judges eight artefacts**: the `"use client"` prologue
124
+ must be present on both `catalog/react` builds and ABSENT from all four
125
+ `catalog/index.*` and all four `catalog/native/index.*` files.
126
+
127
+ ### Known limits, stated plainly
128
+
129
+ There is still **no React Native canary harness** in this workspace (campaign
130
+ decision C7 schedules it for Phase 2), and the real `react-native` package
131
+ cannot be loaded by vitest — it ships Flow-typed source only Metro parses. The
132
+ native tests therefore render against a minimal RN stand-in
133
+ (`catalog/native/__tests__/react-native.stub.tsx`) that maps RN primitives onto
134
+ DOM nodes. That proves rows, order, labels, counts, handlers and every
135
+ announced degradation. It does **not** prove pixel layout, gestures or Hermes —
136
+ those are proven by the consuming Expo app's own build and device smoke.
137
+
138
+ ## 0.8.0 — 2026-09-08
139
+
140
+ **THE ORCHESTRA BADGE IS BACK, in every client at once.**
141
+
142
+ An **Orchestra** is a conductor agent that delegates to member agents at run
143
+ time: choosing one for a step, a chat or a workflow node runs the whole
144
+ ensemble. The Workflow Studio's old hand-rolled picker showed that — a
145
+ conductor marker, the member count, the run mode, the tagline and the member
146
+ titles — because the Python route `GET /agents` stamped it from
147
+ `platform.associations`.
148
+
149
+ Every picker on the platform now reads the RPC `agx_get_list_full`, and that
150
+ RPC had no such column. So the day the pickers unified, EVERY client lost the
151
+ badge at once, and an Orchestra became indistinguishable from an ordinary
152
+ agent in the one place where the difference decides what happens next.
153
+
154
+ The fix is in the read every client shares, not in any client.
155
+
156
+ ### Consumer action (C28)
157
+
158
+ **None for the badge — it appears by itself.** The column is served by the
159
+ platform RPC, this package parses it, and the picker renders it. Update to
160
+ `0.8.0` (hosts on `"latest"` get it on their next install) and any Orchestra in
161
+ the list wears its marker.
162
+
163
+ **One import path changed, compatibly:** `SORT_OPTIONS` now lives in the pure
164
+ `@ai-matrx/agents/catalog` entry. `@ai-matrx/agents/catalog/react` re-exports
165
+ it unchanged, so nothing breaks — but a server-reachable module that only wants
166
+ the five sort labels should import it from `./catalog` and stop dragging the
167
+ `"use client"` bundle into a server graph.
168
+
169
+ **Rendering your own summary of a chosen agent?** Use the exported `Music4Icon`
170
+ and `orchestraLabel` / `orchestraDelegatesLine` / `orchestraDepthLine` rather
171
+ than writing a second vocabulary for the same badge.
172
+
173
+ ### What is new
174
+
175
+ - **`AgentSummary.orchestra: OrchestraBadge | null`** — strict, on every row.
176
+ `{ mode, tagline, depthBudget, memberCount, memberTitles }`, camelCased from
177
+ the new nullable `orchestra jsonb` column.
178
+ - **`AgentRow`** carries the conductor marker: the Orchestra glyph plus the
179
+ member count, titled with the delegates line. Absent on an ordinary agent —
180
+ never a dead or empty chip.
181
+ - **`AgentDetailCard`** leads with the Orchestra block — "Orchestra · supervisor
182
+ · 4 members", the tagline, "Delegates to: …" (collapsing past three into
183
+ "+N more") and the declared delegation depth. It sits ABOVE the description
184
+ because it changes what selecting this agent MEANS.
185
+ - **`orchestraLabel` / `orchestraModeLabel` / `orchestraDelegatesLine` /
186
+ `orchestraDepthLine`** — the Creator words, ported verbatim from the studio
187
+ picker, exported from both entries.
188
+ - **`Music4Icon`** — inlined SVG (C19), the same glyph the studio used.
189
+ - **`assertAgentCatalogSchema`** reports `orchestraColumn: boolean | null`: a
190
+ database that predates the column is VISIBLE, not silently badge-less.
191
+
192
+ ### The platform side
193
+
194
+ Migration `matrx-frontend/migrations/agx_list_orchestra_badge_column.sql` adds
195
+ a trailing nullable `orchestra jsonb` to `agx_get_list`, `agx_get_list_full`
196
+ and `agx_search`, built by ONE new helper `public.agx_orchestra_badges()` —
197
+ the conductor self-edge (`source_type='agent'`, `source_id=target_id`,
198
+ `role='orchestra'`) for mode/tagline/depth_budget, its `role='member'` edges
199
+ for the count and the non-empty labels. Membership, `ORDER BY`, `SECURITY
200
+ DEFINER`, `search_path` and every grant are unchanged; the page is cut BEFORE
201
+ the badge join so `LIMIT`/`OFFSET` still bite on exactly the rows they did.
202
+
203
+ ### Deliberate deltas, said out loud
204
+
205
+ - **`depthBudget` is what the composition DECLARED, and `null` when it declared
206
+ nothing.** The old studio badge printed the EFFECTIVE ceiling, defaulting to
207
+ the platform constant `PROJECTED_AGENT_MAX_RECURSION_DEPTH`. That constant
208
+ lives in the Python that enforces it; copying it into SQL and again into
209
+ TypeScript would be three places to drift. So a composition with no declared
210
+ budget shows no depth line at all, rather than a number nobody wrote.
211
+ - **The literal token "orchestra" is not searchable.** The studio's picker let
212
+ an author type the word and see only ensembles. The canonical scorer this
213
+ package ports (from matrx-frontend) never had that, and adding it would
214
+ change the 295-case parity matrix. Not ported; recorded here rather than lost.
215
+
216
+ ### Proof
217
+
218
+ `catalog/__tests__/orchestra.test.ts` layers conductor rows onto the SAME
219
+ 66-row parity fixture and asserts the badge survives the parser, every sort,
220
+ the user/builtin split and a real filtered-search selector, pinned to literal
221
+ expectations (comparing output to input passes trivially when both are null —
222
+ which is the regression it exists to catch). `catalog/react/picker.test.tsx`
223
+ renders a conductor beside an ordinary agent and asserts the marker, its count,
224
+ its hover text and its ABSENCE on the ordinary row. Both proven RED on a
225
+ deliberate perturbation and GREEN restored.
226
+
227
+
3
228
  ## 0.7.1 — 2026-09-08
4
229
 
5
230
  **0.7.0's two catalog entries did not typecheck together.** `./catalog` and
@@ -30,6 +30,7 @@ __export(catalog_exports, {
30
30
  DEFAULT_AGENT_CATALOG_LABELS: () => DEFAULT_AGENT_CATALOG_LABELS,
31
31
  DEFAULT_AGENT_CONSUMER_STATE: () => DEFAULT_AGENT_CONSUMER_STATE,
32
32
  MandateDefaultRowError: () => MandateDefaultRowError,
33
+ SORT_OPTIONS: () => SORT_OPTIONS,
33
34
  _resetCatalogGlobalState: () => _resetCatalogGlobalState,
34
35
  agentConsumerHasActiveFilters: () => agentConsumerHasActiveFilters,
35
36
  agentListEmptyLabel: () => agentListEmptyLabel,
@@ -61,9 +62,14 @@ __export(catalog_exports, {
61
62
  memoize1: () => memoize1,
62
63
  memoize2: () => memoize2,
63
64
  memoize3: () => memoize3,
65
+ orchestraDelegatesLine: () => orchestraDelegatesLine,
66
+ orchestraDepthLine: () => orchestraDepthLine,
67
+ orchestraLabel: () => orchestraLabel,
68
+ orchestraModeLabel: () => orchestraModeLabel,
64
69
  parseMandateResolution: () => parseMandateResolution,
65
70
  readAgentCatalogRows: () => readAgentCatalogRows,
66
71
  readCachedDefaultRow: () => readCachedDefaultRow,
72
+ readCachedDefaultRowAsync: () => readCachedDefaultRowAsync,
67
73
  readSingleAgentRow: () => readSingleAgentRow,
68
74
  resolveDefaultRow: () => resolveDefaultRow,
69
75
  searchAgentsOnServer: () => searchAgentsOnServer,
@@ -82,7 +88,8 @@ __export(catalog_exports, {
82
88
  sortFilteredAgents: () => sortFilteredAgents,
83
89
  toAgentSummary: () => toAgentSummary,
84
90
  writeAgentFavorite: () => writeAgentFavorite,
85
- writeCachedDefaultRow: () => writeCachedDefaultRow
91
+ writeCachedDefaultRow: () => writeCachedDefaultRow,
92
+ writeCachedDefaultRowAsync: () => writeCachedDefaultRowAsync
86
93
  });
87
94
  module.exports = __toCommonJS(catalog_exports);
88
95
 
@@ -96,6 +103,13 @@ function mandateAgentId(mandateKey) {
96
103
  function isMandateAgentId(id) {
97
104
  return id.startsWith("mandate:");
98
105
  }
106
+ var SORT_OPTIONS = [
107
+ { value: "updated-desc", label: "Recent" },
108
+ { value: "created-desc", label: "Created" },
109
+ { value: "name-asc", label: "A \u2192 Z" },
110
+ { value: "name-desc", label: "Z \u2192 A" },
111
+ { value: "category-asc", label: "Category" }
112
+ ];
99
113
  var AGENT_NONE_SENTINEL = "__none__";
100
114
  var DEFAULT_AGENT_CONSUMER_STATE = {
101
115
  tab: "mine",
@@ -165,6 +179,18 @@ function stringArray(value) {
165
179
  function agentType(value) {
166
180
  return value === "builtin" ? "builtin" : "user";
167
181
  }
182
+ function orchestraBadge(value) {
183
+ if (!isRecord(value)) return null;
184
+ const depth = value["depth_budget"];
185
+ const count = value["member_count"];
186
+ return {
187
+ mode: str(value["mode"]) ?? "supervisor",
188
+ tagline: str(value["tagline"]),
189
+ depthBudget: typeof depth === "number" && Number.isFinite(depth) && depth > 0 ? depth : null,
190
+ memberCount: typeof count === "number" && Number.isFinite(count) && count >= 0 ? count : 0,
191
+ memberTitles: stringArray(value["member_titles"])
192
+ };
193
+ }
168
194
  function toAgentSummary(raw) {
169
195
  if (!isRecord(raw)) return null;
170
196
  const id = str(raw["id"]);
@@ -188,7 +214,8 @@ function toAgentSummary(raw) {
188
214
  updatedAt: str(raw["updated_at"]),
189
215
  isOwner: boolOrNull(raw["is_owner"]),
190
216
  accessLevel: str(raw["access_level"]),
191
- sharedByEmail: str(raw["shared_by_email"])
217
+ sharedByEmail: str(raw["shared_by_email"]),
218
+ orchestra: orchestraBadge(raw["orchestra"])
192
219
  };
193
220
  }
194
221
 
@@ -405,6 +432,51 @@ function writeCachedDefaultRow(storage, resolved) {
405
432
  } catch {
406
433
  }
407
434
  }
435
+ function parseCachedDefaultRow(mandateKey, raw) {
436
+ try {
437
+ if (!raw) return null;
438
+ const parsed = JSON.parse(raw);
439
+ if (!isRecord2(parsed)) return null;
440
+ const holderId = parsed["holderId"];
441
+ const holderName = parsed["holderName"];
442
+ if (typeof holderId !== "string" || typeof holderName !== "string") {
443
+ return null;
444
+ }
445
+ const description = parsed["holderDescription"];
446
+ const provenance = parsed["provenance"];
447
+ const at = parsed["at"];
448
+ return {
449
+ mandateKey,
450
+ holderId,
451
+ holderName,
452
+ holderDescription: typeof description === "string" ? description : null,
453
+ provenance: typeof provenance === "string" ? provenance : "system",
454
+ at: typeof at === "number" ? at : 0
455
+ };
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ async function readCachedDefaultRowAsync(storage, mandateKey) {
461
+ let raw;
462
+ try {
463
+ raw = await storage.getItem(
464
+ CACHE_PREFIX + mandateKey
465
+ );
466
+ } catch {
467
+ return null;
468
+ }
469
+ return parseCachedDefaultRow(mandateKey, raw);
470
+ }
471
+ async function writeCachedDefaultRowAsync(storage, resolved) {
472
+ try {
473
+ await storage.setItem(
474
+ CACHE_PREFIX + resolved.mandateKey,
475
+ JSON.stringify(resolved)
476
+ );
477
+ } catch {
478
+ }
479
+ }
408
480
  function defaultRowFromResolution(resolved) {
409
481
  return {
410
482
  id: mandateAgentId(resolved.mandateKey),
@@ -426,6 +498,9 @@ function defaultRowFromResolution(resolved) {
426
498
  isOwner: null,
427
499
  accessLevel: null,
428
500
  sharedByEmail: null,
501
+ // A mandate default row is a live reference to whichever agent the mandate
502
+ // resolves to; the badge belongs to the RESOLVED agent's own catalog row.
503
+ orchestra: null,
429
504
  mandateKey: resolved.mandateKey
430
505
  };
431
506
  }
@@ -441,7 +516,7 @@ function detectDefaultRowDrift(cached, live) {
441
516
  }
442
517
  async function resolveDefaultRow(args) {
443
518
  const { mandateKey, transport, client, storage, lookupRow } = args;
444
- const cached = readCachedDefaultRow(storage, mandateKey);
519
+ const cached = await readCachedDefaultRowAsync(storage, mandateKey);
445
520
  const verdict = await askTheServer(transport, mandateKey);
446
521
  if (verdict.holderType !== "agent") {
447
522
  throw new MandateDefaultRowError({
@@ -478,7 +553,7 @@ async function resolveDefaultRow(args) {
478
553
  at: Date.now()
479
554
  };
480
555
  const drift = detectDefaultRowDrift(cached, resolved);
481
- writeCachedDefaultRow(storage, resolved);
556
+ await writeCachedDefaultRowAsync(storage, resolved);
482
557
  return { resolved, row: defaultRowFromResolution(resolved), drift };
483
558
  }
484
559
 
@@ -530,10 +605,14 @@ async function assertAgentCatalogSchema(client, options = {}) {
530
605
  const missing = [];
531
606
  const unreachable = [];
532
607
  const answered = [];
608
+ let orchestraColumn = null;
533
609
  for (const fn of names) {
534
610
  const args = DEMANDED_RPCS[fn] ?? {};
535
611
  try {
536
- const { error } = await client.rpc(fn, args);
612
+ const { data, error } = await client.rpc(fn, args);
613
+ if (fn === "agx_get_list_full" && error == null) {
614
+ orchestraColumn = readsOrchestraColumn(data);
615
+ }
537
616
  if (error == null) answered.push(fn);
538
617
  else if (isMissingFunctionError(error)) missing.push(fn);
539
618
  else answered.push(fn);
@@ -573,7 +652,8 @@ async function assertAgentCatalogSchema(client, options = {}) {
573
652
  missing,
574
653
  unreachable,
575
654
  answered,
576
- favoriteWritable
655
+ favoriteWritable,
656
+ orchestraColumn
577
657
  };
578
658
  if (!ok && throwOnViolation) {
579
659
  const parts = [];
@@ -591,6 +671,12 @@ async function assertAgentCatalogSchema(client, options = {}) {
591
671
  }
592
672
  return report;
593
673
  }
674
+ function readsOrchestraColumn(data) {
675
+ if (!Array.isArray(data) || data.length === 0) return null;
676
+ return data.some(
677
+ (row) => typeof row === "object" && row !== null && Object.prototype.hasOwnProperty.call(row, "orchestra")
678
+ );
679
+ }
594
680
  function isPermissionRefusal(error) {
595
681
  if (!error) return false;
596
682
  const code = error.code;
@@ -964,6 +1050,33 @@ function getRegisteredAgentCatalog(catalogId = "default") {
964
1050
  return found;
965
1051
  }
966
1052
 
1053
+ // catalog/orchestra.ts
1054
+ var ORCHESTRA_MODE_LABEL = {
1055
+ supervisor: "supervisor",
1056
+ sequential: "in order",
1057
+ parallel: "together",
1058
+ dag: "custom flow"
1059
+ };
1060
+ function orchestraModeLabel(mode) {
1061
+ return ORCHESTRA_MODE_LABEL[mode] ?? mode;
1062
+ }
1063
+ function orchestraLabel(badge) {
1064
+ const n = badge.memberCount;
1065
+ return `Orchestra \xB7 ${orchestraModeLabel(badge.mode)} \xB7 ${n} ${n === 1 ? "member" : "members"}`;
1066
+ }
1067
+ function orchestraDepthLine(badge) {
1068
+ const n = badge.depthBudget;
1069
+ if (typeof n !== "number" || n <= 0) return null;
1070
+ return `Team members can call helpers up to ${n} ${n === 1 ? "level" : "levels"} deep`;
1071
+ }
1072
+ function orchestraDelegatesLine(badge, limit = 3) {
1073
+ const titles = badge.memberTitles;
1074
+ if (titles.length === 0) return null;
1075
+ const shown = titles.slice(0, limit);
1076
+ const extra = titles.length - shown.length;
1077
+ return `Delegates to: ${shown.join(", ")}${extra > 0 ? ` +${extra} more` : ""}`;
1078
+ }
1079
+
967
1080
  // catalog/labels.ts
968
1081
  function agentListEmptyLabel(tab, labels) {
969
1082
  switch (tab) {