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

dsh-plugins-graph

v1.2.0

Published

LangGraph-style `graph` model tool for DeepSeek Harness — runs state graphs of subagent and pure-JS nodes with static and conditional edges, first-class cycles, and unbounded loops. Ships a web tool card visualizing live execution plus an interactive Grap

Readme

dsh-plugins-graph

LangGraph-style graph work mode for the DeepSeek Harness (DSH) Web GUI.

Run state graphs of subagents and pure-JS nodes with static and conditional edges, first-class cycles, and unbounded loops — plus a web tool card visualising live execution and an interactive Graphs tab with a drag-to-edit editor and an FS-backed persistent library.

┌── chat ──────────────┐  ┌── Graphs tab ──────────────────────┐
│ …                    │  │ graphs  3 live   17 saved        │
│ ▸ graph: revise-loop │  │ �──────────┐  ┌─────────────────�│
│   ● running 1m12s    │  │ ● revise  │  │ edit · revise   ││
│   ◆ writer  ● 412ms  │  │ ● loop    │  │ ◆ writer ◆ rev… ││
│   ◇ bump    ✓ 18ms   │  │ ✓ demo    │  │ ┌─[SVG]────────┐││
│   ◆ reviewer ● ...   │  │ ✓ arch    │  │ │ ●─→●        │││
│   ↳ step scrubber ▬○─ │  │ ✓ …       │  │ │ ▲   ↺  ●─→● │││
│   ▸ output: {…}      │  │ …         │  │ └──────────────┘││
│                     │  │           │  │ [+ agent][+ js]│
│                     │  │           │  │ [save changes] │
└─────────────────────┘  └───────────┴──┴─────────────────┘

Table of contents

  1. Why this plugin
  2. Concepts
  3. Installation
  4. Quick start
  5. The graph tool — full reference
  6. The Graphs tab — what the user sees
  7. The interactive editor
  8. Persistence on disk
  9. For plugin authors — the engine API
  10. Troubleshooting
  11. Compatibility & limitations
  12. License

Why this plugin

Out of the box, DSH gives you one tool per atomic capability (bash, read, web_search, …). The agent calls tools one after another. For tasks that are intrinsically iterative or stateful across many steps — "draft → review → revise → review → ship" being the canonical example — that model is awkward: each turn is independent, each tool result is discarded unless the agent remembers to stuff it into the prompt.

graph adds one model-facing tool that runs a whole subprogram of agent + JS steps as a single call:

  • State machine semantics, not a flat pipeline. Nodes run in a topological frontier; routing can branch back, so cycles and "loop until convergence" are first-class. maxSteps: 0 is genuinely unbounded.
  • Two node kinds. agent nodes delegate to a subagent with its own prompt and optional output schema; js nodes are pure functions of state.
  • Conditional edges. An edge can be static (a → b) or a router — a small JS function body that decides the next node from state (return the node id or "END").
  • Live visualisation. A built-in tool card shows topology + per-node state + step scrubber. A separate Graphs tab lists every live + saved run and lets you open a full editor.
  • Long-term library. Pass persist: true and the definition is saved as <id>.json under $DSH_HOME/graphs/. Open it next session, drag nodes around, click to edit code, save back.

If you've used LangGraph, this will feel familiar: same super-step model, same Pregel-style frontier execution, same "routers are functions" idea.


Concepts

| Term | Meaning | |---|---| | Graph | A spec: name, entry, nodes[], edges[], maxSteps?. Lives either transiently (one-shot) or on disk (.json in the library). | | Node | Either type: "agent" (delegates to a subagent) or type: "js" (pure state transform). Has id, writeTo?, prompt/code, outputSchema?. | | Edge | Either {from, to} (static) or {from, router} (conditional — router is the function BODY of (state, info) => nodeId \| "END" \| [nodeIds], not a full arrow function). | | Super-step | One frontier execution: all currently-frontier nodes run in parallel, all read the same step-start state, writes merge at step end. | | Routing | After each super-step, every just-finished node's outgoing edges (static ∪ router result) determine the next frontier. Loops form naturally. | | END | Reserved edge target meaning "this branch finished." The run ends when every branch hits END. | | One-shot vs persistent | Default runs are ephemeral. Pass persist: true and the spec survives in the library. | | Library | The on-disk store of saved graphs. Each file is one graph, keyed by id (default: derived from name). | | Graphs tab | The conversation-view sidebar panel that lists live + saved graphs and opens the editor/viewer. |


Installation

The package is host-plane + client module. DSH loads it as soon as the package is visible to the profile and the profile has a row that mounts it.

1. Make the package visible

The package is published on npm, so the one-liner is:

dsh plugin --profile web add dsh-plugins-graph

For local development, symlink the checkout into the shared pool (matches what dsh plugin does):

# Symlink the plugin into the shared pool
ln -sfn /Users/max_yang/Projects/AI_Projects/dsh-plugins/plugins/graph \
        ~/.dsh/profiles/node_modules/dsh-plugins-graph

# (Optionally also into the specific profile)
mkdir -p ~/.dsh/profiles/web/node_modules
ln -sfn /Users/max_yang/Projects/AI_Projects/dsh-plugins/plugins/graph \
        ~/.dsh/profiles/web/node_modules/dsh-plugins-graph

Published as dsh-plugins-graph — the npm name, the internal plugin id, and the node_modules mount directory are ALL the same single identity (the @dsh-plugins npm scope is taken, so everything uses the unscoped name).

2. Add the host row

Append to ~/.dsh/profiles/web/cordis.patch.yml (or any patch layer the profile composes — bundles resolve first, then per-profile patches). Use the id-less insert form so the row is appended to the top-level list:

# ~/.dsh/profiles/web/cordis.patch.yml

# LangGraph-style `graph` model tool — agent + JS state graphs with cycles
# and unbounded loops. Host-plane, visible to every agent in every preset.
- insert:
    - id: tool-graph
      name: 'dsh-plugins-graph'
      config:
        provider: spawn     # subagent backend: spawn-in-process
        maxParallel: 6      # agent nodes concurrently per super-step
        allowHostIO: false  # true compiles js/router code raw in the host
                            # realm (require/process/import reachable)

The tool publishes no Cordis Service and registers no slots, so it sits loose in the host composition without needing an isolate realm.

3. Restart dsh web

dsh web --profile web

The host row mounts, the graph tool becomes callable in every session, and the dsh.client module scan picks up client.js, which renders the tool card AND the Graphs tab (since 1.2.0 — see Mounting the Graphs tab below for what the tab can and cannot do without the dynamic host half).


Quick start

In any DSH session, ask the agent to run a simple graph:

Please call the graph tool with this spec, with persist: true:

  • name: "hello-loop"
  • entry: "greet"
  • nodes:
    • {id: "greet", type: "js", code: "return {n: (state.n ?? 0) + 1, msg: 'hello #' + (state.n ?? 0 + 1)}"}
  • edges:
    • {from: "greet", router: "return state.n >= 3 ? 'END' : 'greet'"}
  • maxSteps: 0

Watch the tool card render:

  • A pulsing running badge.
  • The static topology with the entry node lit up.
  • A step scrubber at the bottom — drag it to replay each super-step.
  • The js node outputs (the n counter and msg text) once it settles.
  • The endReason flips to end and the badge turns green.

Now click Graphs in the conversation's right rail. You'll see the run listed under "live", settle into "saved" with name hello-loop, and be openable for editing.


The graph tool — full reference

Parameters

| Field | Type | Required | Default | Meaning | |---|---|---|---|---| | name | string | no | "graph" | Display label; used to derive the persisted id (when persist: true). | | entry | string | yes | — | The node id that runs first. | | nodes | array | yes | — | At least one node. | | edges | array | no | [] | Routing edges. | | state | object | no | {} | Initial shared state (any plain JSON value). | | maxSteps | integer | no | 100 | Super-step budget. 0 = unlimited. | | persist | boolean | no | false | When true and steps > 0, saves the spec to the library. | | dryRun | boolean | no | false | Validate only, returns {endReason: "dry-run", steps: 0, state, trace: [], issues, loops}. Compile errors in js/router code surface as issues here — nothing executes. |

Node schema:

| Field | Type | Notes | |---|---|---| | id | string | Unique within the graph. | | type | "agent" | "js" | Required. | | writeTo | string | State key to store the node's output under. Defaults to the node id. | | description | string | Free-form label. | | prompt | string | (agent only) Template; {state.path.to.value} (or {{ path }}) interpolates from the step-start state — the leading state. names the state itself; numeric indexes ({state.items[0]}) resolve; unknown paths render [missing state key: …] rather than literal text. Interpolation is single-pass: a state value that itself looks like {a.b} is never re-expanded. | | outputSchema | object | (agent only) Object-rooted JSON schema; the subagent's structured result is captured. | | persona | string | (agent only) Per-node persona overriding the parent's persona for this child only. | | provider | string | (agent only) Per-node subagent provider overriding the plugin-level provider config — e.g. route one node to a more capable agent type. | | code | string | (js only) Function body (state, info) => value. Async is awaited; plain objects are shallow-merged into state. Compiled up front — a syntax error fails the run at step 0. Runs in a best-effort sandbox (see below). |

Edge schema:

| Field | Type | Notes | |---|---|---| | from | string | Required. | | to | string | Static target — the id of another node, or "END". | | router | string | Conditional: function body (state, info) => nodeId \| "END" \| [nodeIds]. Return an array for fan-out. Mutually exclusive with to. |

Return schema (canonical)

{
  "endReason": "end" | "max-steps" | "aborted" | "error" | "dry-run",
  "steps": 0,
  "state": { "...": "..." },
  "trace": [
    {
      "step": 1,
      "nodes": [
        { "id": "writer", "status": "ok" | "error", "ms": 412 }
      ],
      "next": ["reviewer"]
    }
  ],
  "issues": ["..."],          // only on dry-run
  "loops": "acyclic | ...",   // only on dry-run
  "error": "..."              // only on error
}

The host adds a nodeOutputs projection into ToolResultNode.meta (so the web card can render full values without parsing the render text). It's {nodeId: value} with each value capped at ~4 KB; values longer than that are replaced by {__truncated: true, preview: "..."}.

Edge cases & guarantees

  • Cycles are allowed. maxSteps: 0 is genuinely unbounded — the run ends only on END routing, exec signal abort, or a thrown error.
  • Agent nodes run in parallel in each super-step, up to maxParallel (default 6 concurrent agent delegations per step; js transforms are pure and cheap, so they are not gated).
  • Router expressions are sync. Returning a Promise is a hard error.
  • Dry-run never executes nodes. It returns {endReason: "dry-run", issues, loops} where loops describes whether the static graph is acyclic / has static cycles / has conditional edges.
  • Sandboxed js/router code. By default code and router bodies compile into a bare vm realm holding the standard ECMAScript intrinsics only — require, process, console, timers and dynamic import() are all unreachable. This is an accident guard, not a security boundary (cross-realm escapes via passed-in objects remain conceivable). Set plugin config allowHostIO: true to compile raw in the host realm exactly as before 1.1.0.
  • Compile checks run up front. validateGraph (and therefore dryRun and the first moments of every run) compiles every code/router string: a syntax error fails at step 0 with the node id, before any agent spends a token.
  • JS node contract: the code body receives (state, info) and returns
    • a plain object → shallow-merged into state (when writeTo is unset),
    • any other value → stored under writeTo ?? id in state,
    • a Promise → awaited.

The Graphs tab — what the user sees

The client half registers a conversation.view slot with id "graphs" — in the dynamic variant via src/dynamic-client.js, and since 1.2.0 in the persistent client-module install via client.js too. A Graphs tab appears in the conversation view showing:

  • Header — live count, saved count.
  • Left pane (list) — one row per graph:
    • ● name for live runs (still in flight)
    • ✓ name for saved graphs (in the library)
    • sub-line: status / node+edge count / savedAt timestamp.
  • Right pane (viewer) — once you click a row:
    • Static topology (BFS layered, cycles legible, conditional routers as dashed stubs).
    • Live: a streaming chips strip showing which agent nodes are running, their elapsed time, and a capped preview of what each finished node produced.
    • Saved (edit mode): a draggable canvas with node port circles.
    • A details panel below the SVG for the currently selected node or edge.
    • open agent session button on live nodes — opens the child session in the main conversation view via sessions.openSubagent.

Mounting the Graphs tab

Since 1.2.0 there is nothing to mount: the client-module install ships the tab. The host row (the graph tool) plus the dsh.client module scan give you the tool card and the Graphs tab in every session, persistently.

What the tab can do depends on where its data comes from:

  • The tool card and settled-run topology always work — they are a pure function of the frozen ToolCallBlock.
  • Live runs and the saved-graph library ride the package-private graph.live / graphLibrary.* RPCs, which are answered by the dynamic host half (src/dynamic-host.js) in a cordis_define mount. The client-module install has no host global: every RPC goes through a guarded handle, the poll silently skips, the library side renders its empty state, and nothing throws.

For the full experience in the current session (live chips, library editing), additionally mount the dynamic variant — both halves pasted into a cordis_define + cordis_run cycle:

Please mount the dsh-plugins-graph dynamic variant in this session. Read src/dynamic-host.js and src/dynamic-client.js from ~/.d.../plugins/graph/ and pass them as code.host / code.client to a cordis_define call. Then cordis_run the returned package id.

Do not double-mount in one session: the dynamic variant registers the same slot ids as the persistent client module (tool card key graph, tab id graphs), and a second registration of the same view target is rejected loudly. One or the other per session — the dynamic mount for a trial, the client-module install for good.


The interactive editor

Click any saved graph → the right pane shows the edit badge. You can:

  • Drag a node — pointer-down anywhere on the node body, drag, release.
  • Drag from a port — the small circle at each node's left or right edge. Drop on another node's port to create a static edge.
  • Add a node — + agent / + js buttons. The new node gets a unique id (node1, node2, … or js1, …) and lands at a default position.
  • Delete — select a node or edge and press Delete / Backspace, or use the red button in the side panel. Deleting a node also deletes every edge that references it (from or to) and reassigns the entry if it was the entry.
  • Edit an edge — click it. The panel shows:
    • static mode → pick the target node from a dropdown (default: the entry).
    • router mode → write the router function body in a textarea; defaults to return "END".
    • Delete the edge.
  • Reset layout — discards drag offsets and recomputes the BFS layout.
  • Save changes — writes the working copy back via graphLibrary.update. The button is dirty-tinted until you save; press it to persist; the underlying file's updatedAt advances.

The editor is read-only while it loads (no half-rendered state mid-fetch).


Persistence on disk

When persist: true is set, the host saves a JSON file under:

$DSH_HOME/graphs/<id>.json   # primary
~/.dsh/graphs/<id>.json      # fallback when DSH_HOME is unset

File format (atomic write via tmp + rename):

{
  "version": "graph-library@1",
  "id": "hello-loop",
  "name": "hello-loop",
  "savedAt": "2026-08-18T17:30:11.502Z",
  "updatedAt": "2026-08-18T17:42:03.811Z",
  "spec": {
    "name": "hello-loop",
    "entry": "greet",
    "nodes": [{ "id": "greet", "type": "js", "code": "return {n: ...}" }],
    "edges": [{ "from": "greet", "router": "return state.n >= 3 ? 'END' : 'greet'" }],
    "maxSteps": 0
  },
  "runtime": {
    "lastEndReason": "end",
    "lastSteps": 4,
    "lastRunAt": "2026-08-18T17:30:11.490Z",
    "lastEditedAt": "2026-08-18T17:42:03.808Z"
  }
}

The runtime block is preserved across edits — only spec changes when you save. Malformed files (bad JSON, missing id) are skipped by list — they don't crash the tab, they just disappear from the count.

To delete a saved graph by hand:

rm ~/.dsh/graphs/<id>.json

For plugin authors — the engine API

If you want to embed the graph engine in your own plugin (the host tool is just a thin wrapper), lib/engine.js and lib/editor.js are both importable, zero-dependency, fully testable modules.

runGraph(spec, hooks, signal)

Executes a graph spec to completion with the supplied hooks.

import { runGraph } from "dsh-plugins-graph/engine";

const outcome = await runGraph(spec, {
  runAgent: async (node, promptText, label) => {
    // delegate to your subagent backend; return
    // { structured?, text, stopReason }
  },
}, execSignal);
// outcome: { endReason, steps, state, trace, error? }

applyEdit(spec, edit) and defaultNodePosition(spec, id)

Pure graph editing primitives used by the editor. Useful if you want to build your own UI on top.

import { applyEdit, defaultNodePosition } from "dsh-plugins-graph/editor";

const next = applyEdit(current, {
  type: "addNode",
  nodeType: "agent",
});
const pos = defaultNodePosition(next, "node3");

Supported edit types: setName, setEntry, addNode, deleteNode, addEdge (static or router), deleteEdge, setEdgeStatic, setEdgeRouter. Bad edits (unknown id, duplicate edge, invalid router code) are no-ops.

Host-side: the graphLibrary Cordis Service

ctx.provide("graphLibrary", libraryInstance);
// library methods: list, get(id), save({spec, runtime?, id?, forceId?}),
// update(id, patch), remove(id)

Other host plugins can inject ctx.graphLibrary to read or modify saved graphs — for example, a "graph-of-the-week" digest, an export-to-dot command, etc.


Troubleshooting

The graph tool isn't available in any session. The host row wasn't loaded. Check that cordis.patch.yml (or your profile's patch layer) has the - insert: [{id: tool-graph, name: 'dsh-plugins-graph', config: {...}}] form, then restart dsh web.

The Graphs tab is missing. Since 1.2.0 client.js registers the tab itself — if it is missing, the loaded bundle predates the restart that picked it up (client bundles are read at dsh web startup; edit client.js → restart dsh web), or the dsh.client scan skipped the package (check window.__DSH_BOOT__.entries in the browser console for dsh-plugins-graph).

A saved graph disappeared. Files at $DSH_HOME/graphs/ (or ~/.dsh/graphs/) — the file might be malformed (invalid JSON). The library skips malformed files silently. Run node -e 'JSON.parse(require("fs").readFileSync(".../<id>.json", "utf8"))' to confirm.

Drag-and-connect doesn't work in the editor. Make sure you're dragging from a port circle (the small dot at the node's left or right edge). Dragging the node body moves the node; dragging the port draws a temporary dashed edge.

Save fails with "save returned no record". The dynamic host RPC graphLibrary.update isn't mounted. In a plain client-module install there is no host at all: the tab degrades to its empty state instead of offering saves. The most likely cause in a dynamic mount is a plugin with only the client half (client.js is loaded automatically) but no host half registering the RPCs — re-mount the dynamic variant; both halves are required for the editor to save.

The router returned the wrong node. Router expressions must be synchronous and return a string (or "END", or an array of strings). Returning a Promise is a hard error. Use state.<key> (not getState()); the router reads the live post-step merged state — do not mutate it (nothing is frozen; writes belong in js node return values).

An agent node is hanging forever. The engine passes exec.signal into hooks.runAgent. Make sure your backend forwards the signal to its subagent and respects abort. By default the agent loop's own abort handling kicks in when the parent call is cancelled.


Compatibility & limitations

  • DSH runtime ≥ 0.1.0-rc.7 (the version this plugin was built against).
  • Node ≥ 18, React ≥ 18.2.0 (peer).
  • The graph engine is single-process. There is no clustering, no cross-process run state. Live aggregation is in-memory.
  • The library is per-DSH_HOME. There is no multi-user isolation and no encryption at rest. Treat ~/.dsh/graphs/ like a workspace artifact, not a secret store.
  • The library skips malformed files silently — it doesn't quarantine them. If you need that, run your own sweep on the dir.
  • No multi-tenant write serialization — two agents editing the same graph in parallel will race. The editor is designed for single-user interaction.

License

MIT — see the repository root.