npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ppazosp/agrex

v0.1.7

Published

Real-time graph visualizer for AI agent execution flows

Downloads

653

Readme


Install

npm install @ppazosp/agrex @xyflow/react react react-dom

Quick start

import { Agrex } from '@ppazosp/agrex'
import 'agrex/styles.css'

const nodes = [
  { id: 'root', type: 'agent', label: 'Researcher', status: 'done' },
  { id: 'ws1', type: 'tool', label: 'web_search', parentId: 'root', status: 'done' },
  { id: 'wf1', type: 'tool', label: 'write_file', parentId: 'root', status: 'done', writes: ['f1'] },
  { id: 'f1', type: 'file', label: 'output.md', parentId: 'wf1', status: 'done' },
]

function App() {
  return <Agrex nodes={nodes} />
}

No manual edges needed. Edges are auto-generated from parentId, reads, and writes.

Streaming mode

For real-time updates from a running agent:

import { Agrex, useAgrex } from '@ppazosp/agrex'
import 'agrex/styles.css'

function App() {
  const agrex = useAgrex()

  useEffect(() => {
    const onStart = (e) => {
      agrex.addNode({ id: e.id, type: 'agent', label: e.name, status: 'running' })
    }
    const onCall = (e) => {
      agrex.addNode({ id: e.id, type: 'tool', label: e.name, parentId: e.agentId, status: 'running' })
    }
    const onDone = (e) => {
      agrex.updateNode(e.id, { status: 'done' })
    }
    agent.on('agent:start', onStart)
    agent.on('tool:call', onCall)
    agent.on('tool:done', onDone)
    return () => {
      agent.off('agent:start', onStart)
      agent.off('tool:call', onCall)
      agent.off('tool:done', onDone)
    }
  }, [])

  return <Agrex instance={agrex} />
}

The full store API:

agrex.addNode(node) // Add a node (no-op if id exists)
agrex.addNodes(nodes) // Add multiple nodes (skips duplicates)
agrex.updateNode(id, updates) // Update status, label, or metadata (metadata is merged, not replaced)
agrex.removeNode(id) // Remove a node and its edges
agrex.clear() // Clear everything
agrex.loadJSON({ nodes }) // Load from snapshot

Auto edges

Edges are derived automatically from node fields. You never need to call addEdge for common patterns:

| Field | Edge type | Direction | | ---------------- | --------- | ------------------- | | parentId | spawn | parent -> child | | reads: ['id'] | read | target -> this node | | writes: ['id'] | write | this node -> target |

// This single addNode call creates both the node AND a spawn edge from 'agent1'
agrex.addNode({ id: 't1', type: 'tool', label: 'search', parentId: 'agent1', status: 'running' })

// This creates the node, a spawn edge from 'wf1', AND a write edge to 'f1'
agrex.addNode({ id: 'f1', type: 'file', label: 'data.json', parentId: 'wf1' })

You can still use addEdge for custom edge types not covered by auto-derivation.

Framework integrations

Vercel AI SDK

import { useChat } from 'ai/react'
import { Agrex, useAgrex } from '@ppazosp/agrex'
import 'agrex/styles.css'

function Chat() {
  const agrex = useAgrex()
  const agentId = 'assistant'

  const { messages } = useChat({
    onToolCall: ({ toolCall }) => {
      agrex.addNode({
        id: toolCall.toolCallId,
        type: 'tool',
        label: toolCall.toolName,
        parentId: agentId,
        status: 'running',
        metadata: { args: toolCall.args, startedAt: Date.now() },
      })
    },
  })

  // Mark tools as done when results arrive
  useEffect(() => {
    for (const msg of messages) {
      for (const part of msg.parts ?? []) {
        if (part.type === 'tool-invocation' && part.state === 'result') {
          agrex.updateNode(part.toolInvocation.toolCallId, {
            status: 'done',
            metadata: { endedAt: Date.now() },
          })
        }
      }
    }
  }, [messages])

  return <Agrex instance={agrex} />
}

Anthropic SDK (Claude)

const agrex = useAgrex()
const agentId = 'claude'

agrex.addNode({ id: agentId, type: 'agent', label: 'Claude', status: 'running' })

const stream = anthropic.messages.stream({
  model: 'claude-sonnet-4-20250514',
  messages,
  tools,
})

stream.on('contentBlockStart', (block) => {
  if (block.content_block.type === 'tool_use') {
    agrex.addNode({
      id: block.content_block.id,
      type: 'tool',
      label: block.content_block.name,
      parentId: agentId,
      status: 'running',
      metadata: { startedAt: Date.now() },
    })
  }
})

stream.on('finalMessage', (message) => {
  for (const block of message.content) {
    if (block.type === 'tool_use') {
      agrex.updateNode(block.id, { status: 'done', metadata: { endedAt: Date.now() } })
    }
  }
  agrex.updateNode(agentId, { status: 'done' })
})

OpenAI SDK

const agrex = useAgrex()
const agentId = 'gpt'

agrex.addNode({ id: agentId, type: 'agent', label: 'GPT-4', status: 'running' })

const stream = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages,
  tools,
  stream: true,
})

const toolCalls = new Map()

for await (const chunk of stream) {
  for (const tc of chunk.choices[0].delta.tool_calls ?? []) {
    if (tc.function?.name && !toolCalls.has(tc.index)) {
      const id = `tool-${tc.index}`
      toolCalls.set(tc.index, id)
      agrex.addNode({
        id,
        type: 'tool',
        label: tc.function.name,
        parentId: agentId,
        status: 'running',
        metadata: { startedAt: Date.now() },
      })
    }
  }
}

// Mark all done after stream completes
for (const id of toolCalls.values()) {
  agrex.updateNode(id, { status: 'done', metadata: { endedAt: Date.now() } })
}
agrex.updateNode(agentId, { status: 'done' })

LangChain / LangGraph

const agrex = useAgrex()

const eventStream = agent.streamEvents(input, { version: 'v2' })

for await (const event of eventStream) {
  switch (event.event) {
    case 'on_chain_start':
      agrex.addNode({
        id: event.run_id,
        type: 'agent',
        label: event.name,
        parentId: event.parent_ids?.[0],
        status: 'running',
      })
      break
    case 'on_tool_start':
      agrex.addNode({
        id: event.run_id,
        type: 'tool',
        label: event.name,
        parentId: event.parent_ids?.[0],
        status: 'running',
        metadata: { input: event.data.input, startedAt: Date.now() },
      })
      break
    case 'on_tool_end':
      agrex.updateNode(event.run_id, {
        status: 'done',
        metadata: { endedAt: Date.now() },
      })
      break
    case 'on_chain_end':
      agrex.updateNode(event.run_id, { status: 'done' })
      break
  }
}

Node types

| Type | Shape | Description | | ------------- | ------------------------ | ------------------------------- | | agent | Rounded rectangle (80px) | Root or top-level agent | | sub_agent | Rounded rectangle (56px) | Delegated sub-agent | | tool | Circle (36px) | Tool call (wrench icon default) | | file | Hexagon (48px) | File artifact | | Custom string | Rounded rectangle (48px) | Falls back to DefaultNode |

Edge types

| Type | Color | Description | | ------- | ------------------ | ----------------------------------------- | | spawn | Default edge color | Agent spawns child (auto from parentId) | | write | Amber | Write to file (auto from writes) | | read | Blue | Read from file (auto from reads) |

Props

interface AgrexProps {
  // Data
  nodes?: AgrexNode[]
  edges?: AgrexEdge[]
  instance?: UseAgrexReturn // from useAgrex() for streaming

  // Callbacks
  onNodeClick?: (node: AgrexNode) => void
  onEdgeClick?: (edge: AgrexEdge) => void

  // Appearance
  theme?: 'dark' | 'light' | 'auto' | ThemeObject
  className?: string

  // Custom rendering
  nodeRenderers?: Record<string, React.ComponentType<AgrexNodeProps>>
  toolIcons?: Record<string, React.ComponentType<{ size: number }>>
  fileIcons?: Record<string, React.ComponentType<{ size: number }>>
  edgeColors?: Record<string, string>

  // UI toggles
  showControls?: boolean // default: true
  showLegend?: boolean // default: true
  showToasts?: boolean // default: true
  showDetailPanel?: boolean // default: true
  showStats?: boolean // default: false
  fitOnUpdate?: boolean // default: true
  keyboardShortcuts?: boolean // default: true
  animateEdges?: boolean // default: true
}

Imperative API

const ref = useRef<AgrexHandle>(null)

ref.current?.fitView()
ref.current?.collapseAll()
ref.current?.expandAll()
ref.current?.toJSON() // { nodes, edges }

<Agrex ref={ref} nodes={nodes} />

Theming

// Built-in themes
<Agrex theme="dark" />
<Agrex theme="light" />
<Agrex theme="auto" /> // follows prefers-color-scheme

// Custom theme (merges with dark)
<Agrex theme={{ background: '#1a1a2e', accent: '#e94560', statusRunning: '#f5a623' }} />

Custom icons

import { Search, FileCode, FileJson } from 'lucide-react'
;<Agrex
  toolIcons={{ web_search: Search, run_tests: TestTube }}
  fileIcons={{ py: FileCode, json: FileJson, md: FileText }}
/>

Timing and cost tracking

agrex.addNode({
  id: 't1',
  type: 'tool',
  label: 'web_search',
  parentId: 'root',
  status: 'running',
  metadata: { startedAt: Date.now() },
})

// metadata is merged — only pass the new keys, existing ones are preserved
agrex.updateNode('t1', {
  status: 'done',
  metadata: { endedAt: Date.now(), tokens: 1500, cost: 0.003 },
})

Node collapsing

Click any agent or sub_agent to collapse/expand children. Use collapseAll() / expandAll() for bulk operations.

JSON import/export

const data = ref.current?.toJSON()
agrex.loadJSON(data)

Keyboard shortcuts

| Key | Action | | --------- | ----------- | | + / = | Zoom in | | - | Zoom out | | 0 | Fit to view |

Layout engines

Agrex uses a built-in radial layout optimized for streaming — nodes are positioned incrementally as they arrive without repositioning existing ones.

Alternative layout engines are available as standalone imports for static or batch scenarios (e.g., JSON import, post-hoc repositioning):

import { forceLayout } from '@ppazosp/agrex/layout/force' // requires d3-force
import { dagreLayout } from '@ppazosp/agrex/layout/dagre' // requires @dagrejs/dagre

Install only the layout deps you need:

npm install d3-force          # for forceLayout
npm install @dagrejs/dagre    # for dagreLayout

Mocks

import { createMockPipeline, replay } from '@ppazosp/agrex/mocks'

const scenario = createMockPipeline('multi-agent')
const controller = replay(agrex, scenario, { speed: 2 })

controller.pause()
controller.resume()
controller.setSpeed(4)
controller.cancel()

Scenarios: research-agent, multi-agent, deep-chain.

Next.js

The package includes "use client". For dynamic import:

const Agrex = dynamic(() => import('agrex').then((m) => m.Agrex), { ssr: false })

License

MIT