Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ interface ForceGraph2DProps {
onSelectionChange?: (nodeId: string | null) => void;
selectedSwarmId?: string | null;
swarmCenterNodeId?: string | null;
/** When true, the graph fits within its parent container instead of being full-screen */
contained?: boolean;
}

export function ForceGraphCanvas2D(props: ForceGraph2DProps) {
Expand Down Expand Up @@ -166,7 +168,7 @@ export function ForceGraphCanvas2D(props: ForceGraph2DProps) {
height,
resolution: 1,
antialias: true,
resizeTo: window,
resizeTo: props.contained ? container : window,
backgroundColor: 0x111111,
});

Expand Down Expand Up @@ -487,7 +489,11 @@ export function ForceGraphCanvas2D(props: ForceGraph2DProps) {
return (
<div
ref={containerRef}
className="bg-background animate-fade animate-delay-1000 fixed inset-0 z-0"
className={
props.contained
? "bg-background h-full w-full"
: "bg-background animate-fade animate-delay-1000 fixed inset-0 z-0"
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client";

import { Loading } from "@torus-ts/ui/components/loading";
import { ForceGraphCanvas2D } from "~/app/(pages)/portal/_components/force-graph-2d/force-graph-2d";
import { getConnectedNodesSwarm } from "~/app/(pages)/portal/_components/force-graph-2d/force-graph-2d-utils";
import { useGraphData } from "~/app/(pages)/portal/_components/force-graph/use-graph-data";
import { useMemo } from "react";

interface AgentConnectionsGraphProps {
agentKey: string;
}
Comment on lines +9 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add JSDoc documentation for the interface.

The interface lacks JSDoc documentation. Per coding guidelines, all code documentation must follow JSDoc standards as defined in @docs/DOCUMENTATION_STYLE.md.

🔎 Suggested JSDoc documentation
+/**
+ * Props for the AgentConnectionsGraph component.
+ */
 interface AgentConnectionsGraphProps {
+  /** The unique key/address of the agent to display connections for */
   agentKey: string;
 }

Based on coding guidelines: Follow JSDoc standards for all code documentation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface AgentConnectionsGraphProps {
agentKey: string;
}
/**
* Props for the AgentConnectionsGraph component.
*/
interface AgentConnectionsGraphProps {
/** The unique key/address of the agent to display connections for */
agentKey: string;
}
🤖 Prompt for AI Agents
In
apps/torus-portal/src/app/(pages)/root-allocator/(expanded-pages)/agent/[slug]/components/agent-connections-graph.tsx
around lines 9 to 11, the AgentConnectionsGraphProps interface is missing JSDoc;
add a JSDoc block immediately above the interface that describes the interface
purpose and documents the agentKey property (type string) with a concise
description per @docs/DOCUMENTATION_STYLE.md conventions (one-line summary,
@property or @param tag for agentKey, and any relevant usage notes).


export function AgentConnectionsGraph({
agentKey,
}: AgentConnectionsGraphProps) {
const { graphData, isLoading, allocatorAddress } = useGraphData();
Comment on lines +13 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add JSDoc documentation for the component.

The component function lacks JSDoc documentation explaining its purpose and behavior.

🔎 Suggested JSDoc documentation
+/**
+ * Renders a filtered force-directed graph showing only nodes and links
+ * connected to the specified agent. Excludes the root allocator node from
+ * the visualization.
+ *
+ * @param props - Component props
+ * @returns A force graph visualization or loading/empty states
+ */
 export function AgentConnectionsGraph({
   agentKey,
 }: AgentConnectionsGraphProps) {

Based on coding guidelines: Follow JSDoc standards for all code documentation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function AgentConnectionsGraph({
agentKey,
}: AgentConnectionsGraphProps) {
const { graphData, isLoading, allocatorAddress } = useGraphData();
/**
* Renders a filtered force-directed graph showing only nodes and links
* connected to the specified agent. Excludes the root allocator node from
* the visualization.
*
* @param props - Component props
* @returns A force graph visualization or loading/empty states
*/
export function AgentConnectionsGraph({
agentKey,
}: AgentConnectionsGraphProps) {
const { graphData, isLoading, allocatorAddress } = useGraphData();
🤖 Prompt for AI Agents
In
apps/torus-portal/src/app/(pages)/root-allocator/(expanded-pages)/agent/[slug]/components/agent-connections-graph.tsx
around lines 13 to 16, the AgentConnectionsGraph component is missing JSDoc; add
a JSDoc block immediately above the function that briefly describes the
component's purpose (renders a graph of agent connections), documents the
agentKey prop with @param including its type and meaning, notes any important
behavior (e.g., it uses useGraphData to fetch
graphData/isLoading/allocatorAddress), and adds an @returns describing the
rendered React element; keep the description concise and follow standard JSDoc
tags and formatting.


// Filter graph data to show only nodes connected to this agent
const filteredGraphData = useMemo(() => {
if (!graphData) return null;

// Find the agent node
const agentNode = graphData.nodes.find((n) => n.id === agentKey);
if (!agentNode) return null;

// Get all connected nodes using the swarm traversal logic
const connectedNodeIds = getConnectedNodesSwarm(
agentKey,
graphData.nodes,
graphData.links,
allocatorAddress,
);

// Filter nodes (exclude allocator from the filtered view)
const filteredNodes = graphData.nodes.filter(
(node) => connectedNodeIds.has(node.id) && node.id !== allocatorAddress,
);

// Filter links to only include those between connected nodes (excluding allocator)
const filteredLinks = graphData.links.filter((link) => {
const sourceId =
typeof link.source === "string"
? link.source
: (link.source as { id: string }).id;
const targetId =
typeof link.target === "string"
? link.target
: (link.target as { id: string }).id;
return (
connectedNodeIds.has(sourceId) &&
connectedNodeIds.has(targetId) &&
sourceId !== allocatorAddress &&
targetId !== allocatorAddress
);
});

// Only show graph if there are connections (more than just the agent itself)
if (filteredNodes.length <= 1) return null;

return {
nodes: filteredNodes,
links: filteredLinks,
};
}, [graphData, agentKey, allocatorAddress]);

if (isLoading) {
return (
<div className="flex h-[400px] items-center justify-center rounded-lg border border-zinc-800 bg-zinc-900/50">
<div className="flex items-center gap-2 text-sm text-zinc-400">
<Loading /> Loading network connections...
</div>
</div>
);
}

if (!filteredGraphData) {
return (
<div className="flex h-[200px] items-center justify-center rounded-lg border border-zinc-800 bg-zinc-900/50">
<p className="text-sm text-zinc-400">No network connections found</p>
</div>
);
}

return (
<div className="relative h-[400px] overflow-hidden rounded-lg border border-zinc-800">
<ForceGraphCanvas2D
graphData={filteredGraphData}
onNodeClick={() => {
/* empty */
}}
allocatorAddress={allocatorAddress}
swarmCenterNodeId={agentKey}
contained
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";
import { PenaltyList } from "../../../_components/penalties-list";
import { AgentConnectionsGraph } from "./components/agent-connections-graph";
import { AgentInfoCard } from "./components/agent-info-card";
import { ExpandedViewSocials } from "./components/expanded-view-socials";

Expand Down Expand Up @@ -160,6 +161,11 @@ export default async function AgentPage({ params }: Readonly<AgentPageProps>) {
</Card>

<MarkdownView source={metadata.description} />

<div className="mt-6">
<h3 className="mb-4 text-lg font-semibold">Sub-Agents Graph</h3>
<AgentConnectionsGraph agentKey={agentKey} />
</div>
</div>

<div className="flex flex-col gap-6 md:w-1/3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface AgentCardProps {
percComputedWeight: number | null;
weightFactor: number | null;
isWhitelisted: boolean;
/** For root agents: count of all connected agents in their swarm */
subagentCount?: number;
}

export function AgentCard(props: Readonly<AgentCardProps>) {
Expand Down Expand Up @@ -118,6 +120,7 @@ export function AgentCard(props: Readonly<AgentCardProps>) {
isMetadataLoading={isMetadataLoading}
userWeightPower={userWeightPower}
isWhitelisted={props.isWhitelisted}
subagentCount={props.subagentCount}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { RouterOutputs } from "@torus-ts/api";
import { AgentItemSkeleton } from "@torus-ts/ui/components/agent-card/agent-card-skeleton-loader";
import { InfiniteList } from "@torus-ts/ui/components/infinite-list";
import { api } from "~/trpc/react";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { AgentCard } from "./agent-card";

type InfiniteAgentData = RouterOutputs["agent"]["infinite"];
Expand Down Expand Up @@ -45,6 +45,9 @@ export function InfiniteAgentList({
},
);

// Fetch agent connection counts for displaying subagent/parent counts
const { data: connectionCounts } = api.agent.agentConnectionCounts.useQuery();

// Log error to console and treat as empty list
useEffect(() => {
if (error) {
Expand All @@ -55,6 +58,12 @@ export function InfiniteAgentList({
const agents: AgentType[] =
data?.pages.flatMap((page: InfiniteAgentData) => page.agents) ?? [];

// Create lookup function for subagent counts
const getSubagentCount = useMemo(() => {
if (!connectionCounts) return (_key: string) => undefined;
return (agentKey: string) => connectionCounts.subagentCounts[agentKey];
}, [connectionCounts]);

return (
<InfiniteList
items={agents}
Expand All @@ -68,6 +77,7 @@ export function InfiniteAgentList({
registrationBlock={agent.registrationBlock}
percComputedWeight={agent.percComputedWeight}
isWhitelisted={agent.isWhitelisted ?? false}
subagentCount={getSubagentCount(agent.key)}
/>
)}
getItemKey={(agent: AgentType, index: number) =>
Expand Down
133 changes: 133 additions & 0 deletions packages/api/src/router/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { and, eq, gte, inArray, isNull, max, sql } from "@torus-ts/db";
import {
agentSchema,
computedAgentWeightSchema,
emissionDistributionTargetsSchema,
namespacePermissionsSchema,
penalizeAgentVotesSchema,
permissionsSchema,
} from "@torus-ts/db/schema";
import type { TRPCRouterRecord } from "@trpc/server";
import { z } from "zod";
Expand Down Expand Up @@ -408,4 +411,134 @@ export const agentRouter = {
penalties: penaltiesByAgent[agent.key] ?? [],
}));
}),

/**
* Returns connection counts for agents:
* - subagentCounts: For root agents, the count of all connected agents in their swarm
* - parentCounts: For all agents, the count of root agents that grant permissions to them
*/
agentConnectionCounts: publicProcedure.query(async ({ ctx }) => {
// 1. Fetch all relationships from multiple sources
// Source 1: Base permissions (grantorAccountId -> granteeAccountId)
const basePermissions = await ctx.db
.select({
grantor: permissionsSchema.grantorAccountId,
grantee: permissionsSchema.granteeAccountId,
})
.from(permissionsSchema)
.where(isNull(permissionsSchema.deletedAt));

// Source 2: Namespace permissions (grantor -> recipient)
const namespaceRelations = await ctx.db
.select({
grantor: permissionsSchema.grantorAccountId,
grantee: namespacePermissionsSchema.recipient,
})
.from(namespacePermissionsSchema)
.innerJoin(
permissionsSchema,
eq(
namespacePermissionsSchema.permissionId,
permissionsSchema.permissionId,
),
)
.where(isNull(permissionsSchema.deletedAt));

// Source 3: Emission distribution targets (grantor -> targetAccountId)
const emissionTargets = await ctx.db
.select({
grantor: permissionsSchema.grantorAccountId,
grantee: emissionDistributionTargetsSchema.targetAccountId,
})
.from(emissionDistributionTargetsSchema)
.innerJoin(
permissionsSchema,
eq(
emissionDistributionTargetsSchema.permissionId,
permissionsSchema.permissionId,
),
)
.where(isNull(permissionsSchema.deletedAt));

// 2. Fetch whitelisted (root) agents
const whitelistedAgents = await ctx.db
.select({ key: agentSchema.key })
.from(agentSchema)
.where(
and(eq(agentSchema.isWhitelisted, true), isNull(agentSchema.deletedAt)),
);

const whitelistedSet = new Set(whitelistedAgents.map((a) => a.key));

// 3. Build adjacency lists from all relationship sources
const adjacencyList = new Map<string, Set<string>>();
const reverseAdjacencyList = new Map<string, Set<string>>();

const addRelation = (grantor: string, grantee: string | null) => {
if (!grantee) return;

// Forward: grantor -> grantee (for subagent traversal)
const grantorNeighbors = adjacencyList.get(grantor);
if (grantorNeighbors) {
grantorNeighbors.add(grantee);
} else {
adjacencyList.set(grantor, new Set([grantee]));
}

// Reverse: grantee -> grantor (for parent lookup)
const granteeNeighbors = reverseAdjacencyList.get(grantee);
if (granteeNeighbors) {
granteeNeighbors.add(grantor);
} else {
reverseAdjacencyList.set(grantee, new Set([grantor]));
}
};

// Add relationships from all sources
for (const p of basePermissions) {
addRelation(p.grantor, p.grantee);
}
for (const p of namespaceRelations) {
addRelation(p.grantor, p.grantee);
}
for (const p of emissionTargets) {
addRelation(p.grantor, p.grantee);
}

// 4. Calculate subagent counts for root agents (BFS traversal)
const subagentCounts: Record<string, number> = {};
for (const rootKey of whitelistedSet) {
const visited = new Set<string>();
const queue = [rootKey];

while (queue.length > 0) {
const current = queue.shift();
if (!current || visited.has(current)) continue;
visited.add(current);

const neighbors = adjacencyList.get(current);
if (neighbors) {
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
}

visited.delete(rootKey); // Don't count self
subagentCounts[rootKey] = visited.size;
}

// 5. Calculate parent counts for all agents (only counting root agent parents)
const parentCounts: Record<string, number> = {};
for (const [grantee, grantors] of reverseAdjacencyList) {
const rootParents = [...grantors].filter((g) => whitelistedSet.has(g));
if (rootParents.length > 0) {
parentCounts[grantee] = rootParents.length;
}
}

return { subagentCounts, parentCounts };
}),
} satisfies TRPCRouterRecord;
14 changes: 13 additions & 1 deletion packages/ui/src/components/agent-card/agent-card-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface AgentCardHeaderProps {
isAgentSelected?: boolean;
isWhitelisted?: boolean;
isMetadataLoading?: boolean;
/** For root agents: count of all connected agents in their swarm */
subagentCount?: number;
}

function AgentBadge({
Expand Down Expand Up @@ -64,6 +66,7 @@ export function AgentCardHeader({
isAgentSelected,
isWhitelisted,
isMetadataLoading = false,
subagentCount,
}: AgentCardHeaderProps) {
const socialsList = buildSocials(socials, website);

Expand All @@ -83,7 +86,16 @@ export function AgentCardHeader({
{name}
</h2>
<div className="flex w-full items-center justify-between gap-1 md:absolute md:inset-x-0 md:top-0">
<AgentCardSocialsInfo socials={socialsList} />
<div className="flex items-center gap-1">
<AgentCardSocialsInfo socials={socialsList} />
{isWhitelisted &&
subagentCount !== undefined &&
subagentCount > 0 && (
<Badge variant="secondary" className="whitespace-nowrap">
{subagentCount} Subagent{subagentCount !== 1 ? "s" : ""}
</Badge>
)}
</div>
<AgentBadge
isAgentSelected={isAgentSelected}
isAgentDelegated={isAgentDelegated}
Expand Down
Loading