Skip to content

Commit 10f77a2

Browse files
committed
feat: display subagents on root allocator card
1 parent 6a819ff commit 10f77a2

File tree

5 files changed

+162
-2
lines changed

5 files changed

+162
-2
lines changed

apps/torus-portal/src/app/(pages)/root-allocator/_components/agent-card.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ interface AgentCardProps {
1818
percComputedWeight: number | null;
1919
weightFactor: number | null;
2020
isWhitelisted: boolean;
21+
/** For root agents: count of all connected agents in their swarm */
22+
subagentCount?: number;
2123
}
2224

2325
export function AgentCard(props: Readonly<AgentCardProps>) {
@@ -118,6 +120,7 @@ export function AgentCard(props: Readonly<AgentCardProps>) {
118120
isMetadataLoading={isMetadataLoading}
119121
userWeightPower={userWeightPower}
120122
isWhitelisted={props.isWhitelisted}
123+
subagentCount={props.subagentCount}
121124
/>
122125
);
123126
}

apps/torus-portal/src/app/(pages)/root-allocator/_components/infinite-agent-list.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { RouterOutputs } from "@torus-ts/api";
44
import { AgentItemSkeleton } from "@torus-ts/ui/components/agent-card/agent-card-skeleton-loader";
55
import { InfiniteList } from "@torus-ts/ui/components/infinite-list";
66
import { api } from "~/trpc/react";
7-
import { useEffect } from "react";
7+
import { useEffect, useMemo } from "react";
88
import { AgentCard } from "./agent-card";
99

1010
type InfiniteAgentData = RouterOutputs["agent"]["infinite"];
@@ -45,6 +45,9 @@ export function InfiniteAgentList({
4545
},
4646
);
4747

48+
// Fetch agent connection counts for displaying subagent/parent counts
49+
const { data: connectionCounts } = api.agent.agentConnectionCounts.useQuery();
50+
4851
// Log error to console and treat as empty list
4952
useEffect(() => {
5053
if (error) {
@@ -55,6 +58,12 @@ export function InfiniteAgentList({
5558
const agents: AgentType[] =
5659
data?.pages.flatMap((page: InfiniteAgentData) => page.agents) ?? [];
5760

61+
// Create lookup function for subagent counts
62+
const getSubagentCount = useMemo(() => {
63+
if (!connectionCounts) return (_key: string) => undefined;
64+
return (agentKey: string) => connectionCounts.subagentCounts[agentKey];
65+
}, [connectionCounts]);
66+
5867
return (
5968
<InfiniteList
6069
items={agents}
@@ -68,6 +77,7 @@ export function InfiniteAgentList({
6877
registrationBlock={agent.registrationBlock}
6978
percComputedWeight={agent.percComputedWeight}
7079
isWhitelisted={agent.isWhitelisted ?? false}
80+
subagentCount={getSubagentCount(agent.key)}
7181
/>
7282
)}
7383
getItemKey={(agent: AgentType, index: number) =>

packages/api/src/router/agent/agent.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { and, eq, gte, inArray, isNull, max, sql } from "@torus-ts/db";
22
import {
33
agentSchema,
44
computedAgentWeightSchema,
5+
emissionDistributionTargetsSchema,
6+
namespacePermissionsSchema,
57
penalizeAgentVotesSchema,
8+
permissionsSchema,
69
} from "@torus-ts/db/schema";
710
import type { TRPCRouterRecord } from "@trpc/server";
811
import { z } from "zod";
@@ -408,4 +411,134 @@ export const agentRouter = {
408411
penalties: penaltiesByAgent[agent.key] ?? [],
409412
}));
410413
}),
414+
415+
/**
416+
* Returns connection counts for agents:
417+
* - subagentCounts: For root agents, the count of all connected agents in their swarm
418+
* - parentCounts: For all agents, the count of root agents that grant permissions to them
419+
*/
420+
agentConnectionCounts: publicProcedure.query(async ({ ctx }) => {
421+
// 1. Fetch all relationships from multiple sources
422+
// Source 1: Base permissions (grantorAccountId -> granteeAccountId)
423+
const basePermissions = await ctx.db
424+
.select({
425+
grantor: permissionsSchema.grantorAccountId,
426+
grantee: permissionsSchema.granteeAccountId,
427+
})
428+
.from(permissionsSchema)
429+
.where(isNull(permissionsSchema.deletedAt));
430+
431+
// Source 2: Namespace permissions (grantor -> recipient)
432+
const namespaceRelations = await ctx.db
433+
.select({
434+
grantor: permissionsSchema.grantorAccountId,
435+
grantee: namespacePermissionsSchema.recipient,
436+
})
437+
.from(namespacePermissionsSchema)
438+
.innerJoin(
439+
permissionsSchema,
440+
eq(
441+
namespacePermissionsSchema.permissionId,
442+
permissionsSchema.permissionId,
443+
),
444+
)
445+
.where(isNull(permissionsSchema.deletedAt));
446+
447+
// Source 3: Emission distribution targets (grantor -> targetAccountId)
448+
const emissionTargets = await ctx.db
449+
.select({
450+
grantor: permissionsSchema.grantorAccountId,
451+
grantee: emissionDistributionTargetsSchema.targetAccountId,
452+
})
453+
.from(emissionDistributionTargetsSchema)
454+
.innerJoin(
455+
permissionsSchema,
456+
eq(
457+
emissionDistributionTargetsSchema.permissionId,
458+
permissionsSchema.permissionId,
459+
),
460+
)
461+
.where(isNull(permissionsSchema.deletedAt));
462+
463+
// 2. Fetch whitelisted (root) agents
464+
const whitelistedAgents = await ctx.db
465+
.select({ key: agentSchema.key })
466+
.from(agentSchema)
467+
.where(
468+
and(eq(agentSchema.isWhitelisted, true), isNull(agentSchema.deletedAt)),
469+
);
470+
471+
const whitelistedSet = new Set(whitelistedAgents.map((a) => a.key));
472+
473+
// 3. Build adjacency lists from all relationship sources
474+
const adjacencyList = new Map<string, Set<string>>();
475+
const reverseAdjacencyList = new Map<string, Set<string>>();
476+
477+
const addRelation = (grantor: string, grantee: string | null) => {
478+
if (!grantee) return;
479+
480+
// Forward: grantor -> grantee (for subagent traversal)
481+
const grantorNeighbors = adjacencyList.get(grantor);
482+
if (grantorNeighbors) {
483+
grantorNeighbors.add(grantee);
484+
} else {
485+
adjacencyList.set(grantor, new Set([grantee]));
486+
}
487+
488+
// Reverse: grantee -> grantor (for parent lookup)
489+
const granteeNeighbors = reverseAdjacencyList.get(grantee);
490+
if (granteeNeighbors) {
491+
granteeNeighbors.add(grantor);
492+
} else {
493+
reverseAdjacencyList.set(grantee, new Set([grantor]));
494+
}
495+
};
496+
497+
// Add relationships from all sources
498+
for (const p of basePermissions) {
499+
addRelation(p.grantor, p.grantee);
500+
}
501+
for (const p of namespaceRelations) {
502+
addRelation(p.grantor, p.grantee);
503+
}
504+
for (const p of emissionTargets) {
505+
addRelation(p.grantor, p.grantee);
506+
}
507+
508+
// 4. Calculate subagent counts for root agents (BFS traversal)
509+
const subagentCounts: Record<string, number> = {};
510+
for (const rootKey of whitelistedSet) {
511+
const visited = new Set<string>();
512+
const queue = [rootKey];
513+
514+
while (queue.length > 0) {
515+
const current = queue.shift();
516+
if (!current || visited.has(current)) continue;
517+
visited.add(current);
518+
519+
const neighbors = adjacencyList.get(current);
520+
if (neighbors) {
521+
for (const neighbor of neighbors) {
522+
if (!visited.has(neighbor)) {
523+
queue.push(neighbor);
524+
}
525+
}
526+
}
527+
}
528+
529+
visited.delete(rootKey); // Don't count self
530+
subagentCounts[rootKey] = visited.size;
531+
}
532+
533+
// 5. Calculate parent counts for all agents (only counting root agent parents)
534+
const parentCounts: Record<string, number> = {};
535+
for (const [grantee, grantors] of reverseAdjacencyList) {
536+
const rootParents = [...grantors].filter((g) => whitelistedSet.has(g));
537+
if (rootParents.length > 0) {
538+
parentCounts[grantee] = rootParents.length;
539+
}
540+
}
541+
542+
return { subagentCounts, parentCounts };
543+
}),
411544
} satisfies TRPCRouterRecord;

packages/ui/src/components/agent-card/agent-card-header.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface AgentCardHeaderProps {
1717
isAgentSelected?: boolean;
1818
isWhitelisted?: boolean;
1919
isMetadataLoading?: boolean;
20+
/** For root agents: count of all connected agents in their swarm */
21+
subagentCount?: number;
2022
}
2123

2224
function AgentBadge({
@@ -64,6 +66,7 @@ export function AgentCardHeader({
6466
isAgentSelected,
6567
isWhitelisted,
6668
isMetadataLoading = false,
69+
subagentCount,
6770
}: AgentCardHeaderProps) {
6871
const socialsList = buildSocials(socials, website);
6972

@@ -83,7 +86,14 @@ export function AgentCardHeader({
8386
{name}
8487
</h2>
8588
<div className="flex w-full items-center justify-between gap-1 md:absolute md:inset-x-0 md:top-0">
86-
<AgentCardSocialsInfo socials={socialsList} />
89+
<div className="flex items-center gap-1">
90+
<AgentCardSocialsInfo socials={socialsList} />
91+
{isWhitelisted && subagentCount !== undefined && subagentCount > 0 && (
92+
<Badge variant="secondary" className="whitespace-nowrap">
93+
{subagentCount} Subagent{subagentCount !== 1 ? "s" : ""}
94+
</Badge>
95+
)}
96+
</div>
8797
<AgentBadge
8898
isAgentSelected={isAgentSelected}
8999
isAgentDelegated={isAgentDelegated}

packages/ui/src/components/agent-card/agent-card.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export interface AgentCardProps {
7777
isMetadataLoading?: boolean;
7878
footerContent?: React.ReactNode;
7979
userWeightPower?: string | bigint | null;
80+
/** For root agents: count of all connected agents in their swarm */
81+
subagentCount?: number;
8082
}
8183

8284
export function AgentCard(props: Readonly<AgentCardProps>) {
@@ -103,6 +105,7 @@ export function AgentCard(props: Readonly<AgentCardProps>) {
103105
isMetadataLoading = false,
104106
footerContent,
105107
userWeightPower,
108+
subagentCount,
106109
} = props;
107110

108111
const cardContent = (
@@ -117,6 +120,7 @@ export function AgentCard(props: Readonly<AgentCardProps>) {
117120
isAgentSelected={isAgentSelected}
118121
isWhitelisted={isWhitelisted}
119122
isMetadataLoading={isMetadataLoading}
123+
subagentCount={subagentCount}
120124
/>
121125

122126
<AgentCardContent

0 commit comments

Comments
 (0)