mirror of
https://github.com/block/buzz.git
synced 2026-11-06 01:11:33 +00:00
feat(desktop): show persona duplication count in the catalog passport
Adding a catalog entry is a local-only write, so true duplication counts
don't exist on the relay — but a member who adds a persona and re-shares
their copy publishes a second catalog head with the same definition.
Count distinct other members with a matching operating prompt and show
it as an honest lower bound ("Duplicated by N other members"). Document
that cross-server runs are invisible from a single relay and what relay
support the full signals would need.
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
82
desktop/src/features/agents/lib/personaAdoption.test.mjs
Normal file
82
desktop/src/features/agents/lib/personaAdoption.test.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
countCatalogRepublishers,
|
||||
personaDefinitionFingerprint,
|
||||
} from "./personaAdoption.ts";
|
||||
|
||||
const WES = "a".repeat(64);
|
||||
const ALICE = "b".repeat(64);
|
||||
const BOB = "c".repeat(64);
|
||||
|
||||
function publication(ownerPubkey, agent = {}) {
|
||||
return {
|
||||
eventId: "e".repeat(64),
|
||||
ownerPubkey,
|
||||
sourcePersonaId: "persona-1",
|
||||
createdAt: 1_700_000_000,
|
||||
agent: {
|
||||
displayName: "Brain",
|
||||
avatarUrl: null,
|
||||
systemPrompt: "You review pull requests carefully.",
|
||||
runtime: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
namePool: [],
|
||||
respondTo: null,
|
||||
parallelism: null,
|
||||
...agent,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("fingerprint keys on the prompt, ignoring whitespace and case", () => {
|
||||
const canonical = personaDefinitionFingerprint({
|
||||
displayName: "Brain",
|
||||
systemPrompt: "You review pull requests carefully.",
|
||||
});
|
||||
assert.equal(
|
||||
personaDefinitionFingerprint({
|
||||
displayName: "Renamed Brain",
|
||||
systemPrompt: " You review pull\nrequests CAREFULLY. ",
|
||||
}),
|
||||
canonical,
|
||||
);
|
||||
// Promptless personas fall back to the display name.
|
||||
assert.equal(
|
||||
personaDefinitionFingerprint({ displayName: " Brain ", systemPrompt: "" }),
|
||||
personaDefinitionFingerprint({ displayName: "brain", systemPrompt: " " }),
|
||||
);
|
||||
});
|
||||
|
||||
test("counts distinct other members with a matching publication", () => {
|
||||
const count = countCatalogRepublishers(
|
||||
[
|
||||
publication(WES), // the original — never counts
|
||||
publication(ALICE),
|
||||
publication(ALICE, { displayName: "Brain (alice)" }), // same member twice
|
||||
publication(BOB, { displayName: "Big Brain" }), // renamed copy, same prompt
|
||||
publication(BOB.replace(/c/gu, "d"), {
|
||||
systemPrompt: "A different persona entirely.",
|
||||
}),
|
||||
],
|
||||
{
|
||||
displayName: "Brain",
|
||||
systemPrompt: "You review pull requests carefully.",
|
||||
publisherPubkey: WES,
|
||||
},
|
||||
);
|
||||
assert.equal(count, 2);
|
||||
});
|
||||
|
||||
test("no publisher and no matches yield zero", () => {
|
||||
assert.equal(
|
||||
countCatalogRepublishers([], {
|
||||
displayName: "Brain",
|
||||
systemPrompt: "prompt",
|
||||
publisherPubkey: null,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
});
|
||||
61
desktop/src/features/agents/lib/personaAdoption.ts
Normal file
61
desktop/src/features/agents/lib/personaAdoption.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Adoption signals for a catalog persona, derived from what the relay can
|
||||
* actually show. Adding an entry to your library is a local-only write, so
|
||||
* true duplication counts don't exist on the relay — but a member who adds a
|
||||
* persona and then re-shares their copy publishes a second catalog head with
|
||||
* the same definition. Counting those re-publications gives an honest lower
|
||||
* bound on how many times the persona has been duplicated.
|
||||
*/
|
||||
|
||||
import type { PersonaCatalogPublication } from "@/features/agents/lib/personaCatalogRelay";
|
||||
|
||||
/**
|
||||
* The identity of a persona definition for adoption matching: its operating
|
||||
* prompt when it has one (whitespace-insensitive), otherwise its display name.
|
||||
* Copies keep the source prompt verbatim, so this is the strongest signal a
|
||||
* publication descends from the same definition.
|
||||
*/
|
||||
export function personaDefinitionFingerprint(definition: {
|
||||
displayName: string;
|
||||
systemPrompt: string;
|
||||
}): string {
|
||||
const prompt = definition.systemPrompt.replace(/\s+/gu, " ").trim();
|
||||
if (prompt.length > 0) {
|
||||
return `prompt:${prompt.toLowerCase()}`;
|
||||
}
|
||||
return `name:${definition.displayName.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Distinct members other than the publisher who have re-published this
|
||||
* persona's definition to the community catalog. A lower bound on adoption:
|
||||
* members who added the persona but never re-shared their copy stay
|
||||
* invisible.
|
||||
*/
|
||||
export function countCatalogRepublishers(
|
||||
publications: readonly PersonaCatalogPublication[],
|
||||
entry: {
|
||||
displayName: string;
|
||||
systemPrompt: string;
|
||||
publisherPubkey: string | null;
|
||||
},
|
||||
): number {
|
||||
const fingerprint = personaDefinitionFingerprint(entry);
|
||||
const publisher = entry.publisherPubkey?.toLowerCase() ?? null;
|
||||
const republishers = new Set<string>();
|
||||
for (const publication of publications) {
|
||||
const owner = publication.ownerPubkey.toLowerCase();
|
||||
if (owner === publisher) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
personaDefinitionFingerprint({
|
||||
displayName: publication.agent.displayName,
|
||||
systemPrompt: publication.agent.systemPrompt,
|
||||
}) === fingerprint
|
||||
) {
|
||||
republishers.add(owner);
|
||||
}
|
||||
}
|
||||
return republishers.size;
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as React from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { IdCard } from "lucide-react";
|
||||
import { CopyPlus, IdCard } from "lucide-react";
|
||||
|
||||
import {
|
||||
useManagedAgentsQuery,
|
||||
useRelayAgentsQuery,
|
||||
} from "@/features/agents/hooks";
|
||||
import { countCatalogRepublishers } from "@/features/agents/lib/personaAdoption";
|
||||
import { usePersonaCatalogQuery } from "@/features/agents/lib/usePersonaCatalogRelay";
|
||||
import { useCommunities } from "@/features/communities/useCommunities";
|
||||
import { useAgentPassportBadges } from "@/features/passport/hooks/useAgentPassportBadges";
|
||||
import { PassportBadgeList } from "@/features/passport/ui/PassportBadges";
|
||||
import {
|
||||
@@ -123,6 +126,27 @@ export function PersonaCatalogPassport({
|
||||
const firstAgent = deployedAgents[0] ?? null;
|
||||
const { badges } = useAgentPassportBadges(firstAgent?.pubkey ?? null);
|
||||
|
||||
// Adoption: members who added this persona and re-shared their copy publish
|
||||
// a second catalog head with the same definition. Adds that were never
|
||||
// re-shared are local-only, so this is an honest lower bound — and
|
||||
// cross-server runs are invisible from a single relay entirely.
|
||||
const { activeCommunity } = useCommunities();
|
||||
const catalogQuery = usePersonaCatalogQuery(activeCommunity?.id ?? null);
|
||||
const republisherCount = React.useMemo(
|
||||
() =>
|
||||
countCatalogRepublishers(catalogQuery.data ?? [], {
|
||||
displayName: persona.displayName,
|
||||
publisherPubkey,
|
||||
systemPrompt: persona.systemPrompt,
|
||||
}),
|
||||
[
|
||||
catalogQuery.data,
|
||||
persona.displayName,
|
||||
persona.systemPrompt,
|
||||
publisherPubkey,
|
||||
],
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const openPassport = React.useCallback(
|
||||
(pubkey: string) => {
|
||||
@@ -195,6 +219,17 @@ export function PersonaCatalogPassport({
|
||||
appears here once one is.
|
||||
</p>
|
||||
)}
|
||||
{republisherCount > 0 ? (
|
||||
<p
|
||||
className="flex items-center gap-2 text-sm text-muted-foreground"
|
||||
data-testid="persona-catalog-adoption"
|
||||
>
|
||||
<CopyPlus className="h-3.5 w-3.5 shrink-0" />
|
||||
Duplicated by {republisherCount} other{" "}
|
||||
{republisherCount === 1 ? "member" : "members"} — re-shared to
|
||||
this community’s catalog.
|
||||
</p>
|
||||
) : null}
|
||||
{publisherPubkey ? (
|
||||
<div>
|
||||
<Button
|
||||
|
||||
@@ -70,13 +70,26 @@ One badge, three tiers, based on days since the agent's oldest note on record.
|
||||
want portable, attestable badges (e.g. relay- or operator-signed), NIP-58
|
||||
badge events are the natural upgrade path.
|
||||
|
||||
## Adoption (catalog entries)
|
||||
|
||||
The agent catalog's passport section shows a duplication count: distinct
|
||||
members other than the publisher who re-published the same persona definition
|
||||
(matched by operating prompt) to the community catalog. Adding an entry is a
|
||||
local-only write, so members who added but never re-shared stay invisible —
|
||||
the count is an honest lower bound, like everything else here. Engine:
|
||||
`features/agents/lib/personaAdoption.ts`.
|
||||
|
||||
## Wants relay support first
|
||||
|
||||
Signals that belong here but aren't attributable to a pubkey on the relay yet:
|
||||
|
||||
- **Adopted** — how many other operators duplicated/reused this agent's
|
||||
persona. Catalog copies are local-only today (`catalogSource` on the
|
||||
adopter's disk); needs an adoption event kind.
|
||||
- **Adopted** — the full duplication count (every add, not just re-shares).
|
||||
Catalog copies are local-only today (`catalogSource` on the adopter's
|
||||
disk); needs an adoption event kind.
|
||||
- **Runs on other servers** — a community is one relay, and the client has no
|
||||
cross-relay registry to ask. Needs either portable operator-signed
|
||||
attestations (NIP-58 style) that travel with the agent, or a directory
|
||||
service relays opt into.
|
||||
- **Workhorse** — completed agent turns/sessions. Turn completion is a live
|
||||
observer signal, not a persisted count.
|
||||
- **Automated** — workflow runs executed. `WorkflowRun` has no trigger
|
||||
|
||||
@@ -65,6 +65,15 @@ test("catalog entry shows the deployed agent's passport record", async ({
|
||||
sourcePersonaId: "nadia-persona",
|
||||
systemPrompt: "Ship relay fixes and review incoming pull requests.",
|
||||
}),
|
||||
// Alice re-shared her copy of the persona (same operating prompt) —
|
||||
// that is the observable trace of a duplication.
|
||||
createCatalogEvent({
|
||||
displayName: "nadia (fork)",
|
||||
id: "6".repeat(64),
|
||||
ownerPubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
sourcePersonaId: "nadia-persona-fork",
|
||||
systemPrompt: "Ship relay fixes and review incoming pull requests.",
|
||||
}),
|
||||
],
|
||||
relayAgents: [
|
||||
{
|
||||
@@ -75,6 +84,8 @@ test("catalog entry shows the deployed agent's passport record", async ({
|
||||
],
|
||||
});
|
||||
await openCatalog(page);
|
||||
// Both entries share the prompt; "nadia" sorts before "nadia (fork)" and is
|
||||
// auto-selected, so the detail pane shows the original.
|
||||
|
||||
const passportSection = page.getByTestId("persona-catalog-passport");
|
||||
await expect(passportSection).toBeVisible();
|
||||
@@ -95,6 +106,11 @@ test("catalog entry shows the deployed agent's passport record", async ({
|
||||
passportSection.getByTestId("persona-catalog-publisher-passport"),
|
||||
).toHaveText(/View your passport/);
|
||||
|
||||
// Alice's re-shared copy of the same prompt counts as a duplication.
|
||||
await expect(
|
||||
passportSection.getByTestId("persona-catalog-adoption"),
|
||||
).toHaveText(/Duplicated by 1 other member/);
|
||||
|
||||
await waitForAnimations(page);
|
||||
await page
|
||||
.getByTestId("persona-catalog-detail-pane")
|
||||
|
||||
Reference in New Issue
Block a user