@ai-matrx/agents 0.7.1 → 0.8.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,95 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 — 2026-09-08
4
+
5
+ **THE ORCHESTRA BADGE IS BACK, in every client at once.**
6
+
7
+ An **Orchestra** is a conductor agent that delegates to member agents at run
8
+ time: choosing one for a step, a chat or a workflow node runs the whole
9
+ ensemble. The Workflow Studio's old hand-rolled picker showed that — a
10
+ conductor marker, the member count, the run mode, the tagline and the member
11
+ titles — because the Python route `GET /agents` stamped it from
12
+ `platform.associations`.
13
+
14
+ Every picker on the platform now reads the RPC `agx_get_list_full`, and that
15
+ RPC had no such column. So the day the pickers unified, EVERY client lost the
16
+ badge at once, and an Orchestra became indistinguishable from an ordinary
17
+ agent in the one place where the difference decides what happens next.
18
+
19
+ The fix is in the read every client shares, not in any client.
20
+
21
+ ### Consumer action (C28)
22
+
23
+ **None for the badge — it appears by itself.** The column is served by the
24
+ platform RPC, this package parses it, and the picker renders it. Update to
25
+ `0.8.0` (hosts on `"latest"` get it on their next install) and any Orchestra in
26
+ the list wears its marker.
27
+
28
+ **One import path changed, compatibly:** `SORT_OPTIONS` now lives in the pure
29
+ `@ai-matrx/agents/catalog` entry. `@ai-matrx/agents/catalog/react` re-exports
30
+ it unchanged, so nothing breaks — but a server-reachable module that only wants
31
+ the five sort labels should import it from `./catalog` and stop dragging the
32
+ `"use client"` bundle into a server graph.
33
+
34
+ **Rendering your own summary of a chosen agent?** Use the exported `Music4Icon`
35
+ and `orchestraLabel` / `orchestraDelegatesLine` / `orchestraDepthLine` rather
36
+ than writing a second vocabulary for the same badge.
37
+
38
+ ### What is new
39
+
40
+ - **`AgentSummary.orchestra: OrchestraBadge | null`** — strict, on every row.
41
+ `{ mode, tagline, depthBudget, memberCount, memberTitles }`, camelCased from
42
+ the new nullable `orchestra jsonb` column.
43
+ - **`AgentRow`** carries the conductor marker: the Orchestra glyph plus the
44
+ member count, titled with the delegates line. Absent on an ordinary agent —
45
+ never a dead or empty chip.
46
+ - **`AgentDetailCard`** leads with the Orchestra block — "Orchestra · supervisor
47
+ · 4 members", the tagline, "Delegates to: …" (collapsing past three into
48
+ "+N more") and the declared delegation depth. It sits ABOVE the description
49
+ because it changes what selecting this agent MEANS.
50
+ - **`orchestraLabel` / `orchestraModeLabel` / `orchestraDelegatesLine` /
51
+ `orchestraDepthLine`** — the Creator words, ported verbatim from the studio
52
+ picker, exported from both entries.
53
+ - **`Music4Icon`** — inlined SVG (C19), the same glyph the studio used.
54
+ - **`assertAgentCatalogSchema`** reports `orchestraColumn: boolean | null`: a
55
+ database that predates the column is VISIBLE, not silently badge-less.
56
+
57
+ ### The platform side
58
+
59
+ Migration `matrx-frontend/migrations/agx_list_orchestra_badge_column.sql` adds
60
+ a trailing nullable `orchestra jsonb` to `agx_get_list`, `agx_get_list_full`
61
+ and `agx_search`, built by ONE new helper `public.agx_orchestra_badges()` —
62
+ the conductor self-edge (`source_type='agent'`, `source_id=target_id`,
63
+ `role='orchestra'`) for mode/tagline/depth_budget, its `role='member'` edges
64
+ for the count and the non-empty labels. Membership, `ORDER BY`, `SECURITY
65
+ DEFINER`, `search_path` and every grant are unchanged; the page is cut BEFORE
66
+ the badge join so `LIMIT`/`OFFSET` still bite on exactly the rows they did.
67
+
68
+ ### Deliberate deltas, said out loud
69
+
70
+ - **`depthBudget` is what the composition DECLARED, and `null` when it declared
71
+ nothing.** The old studio badge printed the EFFECTIVE ceiling, defaulting to
72
+ the platform constant `PROJECTED_AGENT_MAX_RECURSION_DEPTH`. That constant
73
+ lives in the Python that enforces it; copying it into SQL and again into
74
+ TypeScript would be three places to drift. So a composition with no declared
75
+ budget shows no depth line at all, rather than a number nobody wrote.
76
+ - **The literal token "orchestra" is not searchable.** The studio's picker let
77
+ an author type the word and see only ensembles. The canonical scorer this
78
+ package ports (from matrx-frontend) never had that, and adding it would
79
+ change the 295-case parity matrix. Not ported; recorded here rather than lost.
80
+
81
+ ### Proof
82
+
83
+ `catalog/__tests__/orchestra.test.ts` layers conductor rows onto the SAME
84
+ 66-row parity fixture and asserts the badge survives the parser, every sort,
85
+ the user/builtin split and a real filtered-search selector, pinned to literal
86
+ expectations (comparing output to input passes trivially when both are null —
87
+ which is the regression it exists to catch). `catalog/react/picker.test.tsx`
88
+ renders a conductor beside an ordinary agent and asserts the marker, its count,
89
+ its hover text and its ABSENCE on the ordinary row. Both proven RED on a
90
+ deliberate perturbation and GREEN restored.
91
+
92
+
3
93
  ## 0.7.1 — 2026-09-08
4
94
 
5
95
  **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,6 +62,10 @@ __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,
@@ -96,6 +101,13 @@ function mandateAgentId(mandateKey) {
96
101
  function isMandateAgentId(id) {
97
102
  return id.startsWith("mandate:");
98
103
  }
104
+ var SORT_OPTIONS = [
105
+ { value: "updated-desc", label: "Recent" },
106
+ { value: "created-desc", label: "Created" },
107
+ { value: "name-asc", label: "A \u2192 Z" },
108
+ { value: "name-desc", label: "Z \u2192 A" },
109
+ { value: "category-asc", label: "Category" }
110
+ ];
99
111
  var AGENT_NONE_SENTINEL = "__none__";
100
112
  var DEFAULT_AGENT_CONSUMER_STATE = {
101
113
  tab: "mine",
@@ -165,6 +177,18 @@ function stringArray(value) {
165
177
  function agentType(value) {
166
178
  return value === "builtin" ? "builtin" : "user";
167
179
  }
180
+ function orchestraBadge(value) {
181
+ if (!isRecord(value)) return null;
182
+ const depth = value["depth_budget"];
183
+ const count = value["member_count"];
184
+ return {
185
+ mode: str(value["mode"]) ?? "supervisor",
186
+ tagline: str(value["tagline"]),
187
+ depthBudget: typeof depth === "number" && Number.isFinite(depth) && depth > 0 ? depth : null,
188
+ memberCount: typeof count === "number" && Number.isFinite(count) && count >= 0 ? count : 0,
189
+ memberTitles: stringArray(value["member_titles"])
190
+ };
191
+ }
168
192
  function toAgentSummary(raw) {
169
193
  if (!isRecord(raw)) return null;
170
194
  const id = str(raw["id"]);
@@ -188,7 +212,8 @@ function toAgentSummary(raw) {
188
212
  updatedAt: str(raw["updated_at"]),
189
213
  isOwner: boolOrNull(raw["is_owner"]),
190
214
  accessLevel: str(raw["access_level"]),
191
- sharedByEmail: str(raw["shared_by_email"])
215
+ sharedByEmail: str(raw["shared_by_email"]),
216
+ orchestra: orchestraBadge(raw["orchestra"])
192
217
  };
193
218
  }
194
219
 
@@ -426,6 +451,9 @@ function defaultRowFromResolution(resolved) {
426
451
  isOwner: null,
427
452
  accessLevel: null,
428
453
  sharedByEmail: null,
454
+ // A mandate default row is a live reference to whichever agent the mandate
455
+ // resolves to; the badge belongs to the RESOLVED agent's own catalog row.
456
+ orchestra: null,
429
457
  mandateKey: resolved.mandateKey
430
458
  };
431
459
  }
@@ -530,10 +558,14 @@ async function assertAgentCatalogSchema(client, options = {}) {
530
558
  const missing = [];
531
559
  const unreachable = [];
532
560
  const answered = [];
561
+ let orchestraColumn = null;
533
562
  for (const fn of names) {
534
563
  const args = DEMANDED_RPCS[fn] ?? {};
535
564
  try {
536
- const { error } = await client.rpc(fn, args);
565
+ const { data, error } = await client.rpc(fn, args);
566
+ if (fn === "agx_get_list_full" && error == null) {
567
+ orchestraColumn = readsOrchestraColumn(data);
568
+ }
537
569
  if (error == null) answered.push(fn);
538
570
  else if (isMissingFunctionError(error)) missing.push(fn);
539
571
  else answered.push(fn);
@@ -573,7 +605,8 @@ async function assertAgentCatalogSchema(client, options = {}) {
573
605
  missing,
574
606
  unreachable,
575
607
  answered,
576
- favoriteWritable
608
+ favoriteWritable,
609
+ orchestraColumn
577
610
  };
578
611
  if (!ok && throwOnViolation) {
579
612
  const parts = [];
@@ -591,6 +624,12 @@ async function assertAgentCatalogSchema(client, options = {}) {
591
624
  }
592
625
  return report;
593
626
  }
627
+ function readsOrchestraColumn(data) {
628
+ if (!Array.isArray(data) || data.length === 0) return null;
629
+ return data.some(
630
+ (row) => typeof row === "object" && row !== null && Object.prototype.hasOwnProperty.call(row, "orchestra")
631
+ );
632
+ }
594
633
  function isPermissionRefusal(error) {
595
634
  if (!error) return false;
596
635
  const code = error.code;
@@ -964,6 +1003,33 @@ function getRegisteredAgentCatalog(catalogId = "default") {
964
1003
  return found;
965
1004
  }
966
1005
 
1006
+ // catalog/orchestra.ts
1007
+ var ORCHESTRA_MODE_LABEL = {
1008
+ supervisor: "supervisor",
1009
+ sequential: "in order",
1010
+ parallel: "together",
1011
+ dag: "custom flow"
1012
+ };
1013
+ function orchestraModeLabel(mode) {
1014
+ return ORCHESTRA_MODE_LABEL[mode] ?? mode;
1015
+ }
1016
+ function orchestraLabel(badge) {
1017
+ const n = badge.memberCount;
1018
+ return `Orchestra \xB7 ${orchestraModeLabel(badge.mode)} \xB7 ${n} ${n === 1 ? "member" : "members"}`;
1019
+ }
1020
+ function orchestraDepthLine(badge) {
1021
+ const n = badge.depthBudget;
1022
+ if (typeof n !== "number" || n <= 0) return null;
1023
+ return `Team members can call helpers up to ${n} ${n === 1 ? "level" : "levels"} deep`;
1024
+ }
1025
+ function orchestraDelegatesLine(badge, limit = 3) {
1026
+ const titles = badge.memberTitles;
1027
+ if (titles.length === 0) return null;
1028
+ const shown = titles.slice(0, limit);
1029
+ const extra = titles.length - shown.length;
1030
+ return `Delegates to: ${shown.join(", ")}${extra > 0 ? ` +${extra} more` : ""}`;
1031
+ }
1032
+
967
1033
  // catalog/labels.ts
968
1034
  function agentListEmptyLabel(tab, labels) {
969
1035
  switch (tab) {