Search by

spora-ai / spora-plugin-team-graph

fabeat

Per-principal directed graph of agent spawning relationships, rendered in the admin SPA via Mermaid.

Package info

github.com/spora-ai/spora-plugin-team-graph

Type:spora-plugin

pkg:composer/spora-ai/spora-plugin-team-graph

Statistics

Installs: 171

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-10-22 13:56 UTC

This package is auto-updated.

Last update: 2026-11-05 21:48:37 UTC


README

A per-principal directed graph of agent spawning relationships for Spora. The endpoint GET /api/v1/plugins/team-graph/graph?principal_id=<id> returns the principal's agents (nodes) and the sub_agent connections between them (edges); the admin SPA renders the edges as a Mermaid 10 flowchart and overlays the node cards as real DOM at /apps/team-graph.

Read-only in v1 — no LLM-callable tools, no migrations, no agent templates.

Install

composer require spora-ai/spora-plugin-team-graph
composer require spora-ai/spora-plugin-team-graph-frontend

Two packages, as for any plugin with an operator-facing panel. This PHP package does not require the frontend half, so the app must require spora-ai/spora-plugin-team-graph-frontend itself — the explicit command above is a no-op if the dependency is ever pulled in transitively. The frontend package (type: spora-plugin-frontend) is what the SporaPluginFrontendInstaller drops into public/plugins/team-graph/; without it the app entry appears but the panel 404s on the bundle.

Open it

After composer install (which triggers the installer's first-run migration) and the regular bin/spora spora:install step, the Team Graph entry appears under the admin panel's sidebar. It serves from /apps/team-graph.

Who can open which graph

The endpoint is gated on visibility, not control — the same rule GET /api/v1/principals/me applies, so the panel's own principal picker can never offer a principal the endpoint then refuses. A caller may read:

  • their own user-principal, and
  • the group-principal of every group they belong to, at any role (member included) — a member can already list and open every agent in the group, so the graph shows them nothing they could not see anyway.

Anything else is a 403 FORBIDDEN. Group owner/admin ("control") is the tier the write paths use — agent transfer, group settings — and is deliberately not required here.

Architecture

  • src/TeamGraphPlugin.php — entry point; wires DI bindings (TeamGraphService, NodeResolver, EdgeResolver, TeamGraphController, plus the ToolConfigServiceInterface alias PHP-DI cannot infer) via ContainerBuildingEvent and registers the single GET route via RoutesRegisteringEvent.
  • routes/team-graph.php — the route path and its middleware (AuthMiddleware, CsrfMiddleware) in one place, loaded through Composer's autoload.files because it sits outside PSR-4's src/.
  • src/TeamGraphApp.php — admin-panel metadata (VueAppInterface).
  • src/Http/TeamGraphController.php — GET …/graph?principal_id=… → data envelope; 401 / 403 / 422 mapping.
  • src/Services/TeamGraphService.php — principal visibility gate + envelope assembly.
  • src/Services/NodeResolver.php — agent aggregation in one SQL pass (active chats, 24h recent, latest in-flight status, and the AgentPictureService wire shape resolved inline so there is no N+1 against agent_pictures / media_assets).
  • src/Services/EdgeResolver.php — one edge per (source, target) pair in each source agent's configured allowed_target_agents allowlist for SubAgentTool, read through the same ToolConfigServiceInterface::getEffectiveSettings() cascade the tool checks at runtime. So the graph shows connections that are configured but have never fired, and drops cross-principal targets the way SubAgentTool::sharePrincipal() would. sub_agent tool_calls are read only to enrich those edges with count_24h (24h window) and last_invoked_at (7d window) — never to add or remove one.

Frontend contract

The host SPA loads the bundle from /plugins/team-graph/main.js (TeamGraphApp::entry()), which must be the frontend package's build.lib.fileName(). The bundle installs a mount/unmount contract on window.SporaAppTeamGraph:

window.SporaAppTeamGraph = {
    mount: (target: HTMLElement, hostContext: PluginHostContext) => void | Promise<void>,
    unmount: (target: HTMLElement) => void,
}

hostContext carries the host's api, pinia, theme, route and router; the plugin installs its own Pinia for plugin-local state and branches the host client in through src/api/client.ts.

Translations

This plugin ships a German catalogue at locales/de.json, declared through translationsPaths() in src/TeamGraphPlugin.php. The host merges it under core's own catalogue, so a German install reads the plugin's operator copy in German.

The key is the English source string, byte for byte — editing the English in the source changes the key, so the old entry stops matching and the new English has no entry. The gate is this repository's own tests/Unit/LocaleCatalogTest.php: it fails on an empty translation, on a translation identical to its key, and on a key the source no longer produces.

Translated

  • the admin app's displayName() and description()
  • the plugin.json description

Never translated — these strings are read by the model, so the operator sees exactly what the model sees:

  • #[Tool].description
  • #[ToolOperation].description
  • #[ToolParameter].description
  • every #[ToolSetting].label and its option labels, including the labels of settings that carry exposeToLlm: true

See the plugin author guide → Translations.

Tests

composer test:parallel

The suite covers:

  • TeamGraphServiceTest — drives the service, both resolvers and the envelope against the same temporary SQLite database the feature suite uses, with only ToolConfigServiceInterface mocked so each scenario can script its own allowlist. Areas: node aggregates and status, configured-vs-observed edge emission, tool_calls enrichment, cross-principal and archived filtering, profile_picture wire shape (palette fallback, variant_key derivation), and the principal-visibility refusal.
  • TeamGraphControllerTest — feature tests against a temporary SQLite database (auth gate, principal visibility per group role, validation).
  • TeamGraphPluginTest — wiring: event subscriptions, the DI bindings, the registered route with its middleware, and the app's VueAppInterface contract.

Plan

See spora-workspace/plans/spora-plugin-team-graph.md for the full design context, including the prototype set and the risk register.