DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

snail30xx /

snail30xx/dsh-graph-runtime

Verified

Graph runtime capabilities for the DeepSeek Harness: register LangGraph compiled graphs as DSH tools, plus a graph-driven routing agent.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@86bb2f84

dsh-graph-runtime

English | 中文

Graph runtime capabilities for the DeepSeek Harness (DSH), published as a standalone Cordis plugin/bundle.

Current capabilities:

  • Graph definition and auto-mounting: define an "extended StateGraph" with defineGraph — a graph factory plus a registration declaration (whether it becomes a DSH tool, whether it joins the registry) plus a tool-argument validation hook. Loading the module completes registration: at plugin startup the already-defined graphs auto-mount, and later definitions mount immediately; developers make no service calls at all. Tool name/description are auto-discovered (compile({ name, description }) or structural synthesis), compilation is lazy and memoized (the registration path has zero compile side effects), and parameter validation, cancellation forwarding, and result rendering are all owned by this plugin.
  • GraphRoutingAgent: a graph-driven routing agent. It hands the graphTools registry (optionally filtered) to the LLM and requires a tool to be chosen; the returned tool call goes through existence, JSON, schema, and author validate checks, retrying with the rejection reason on failure (default 3, configurable); once it passes, the corresponding graph executes. Every chosen-tool execution is wrapped by the graphTools/pre-execute / graphTools/post-execute waterfall extension points (gating, auditing, result transforms).

Installation and wiring

# After publishing
npm install dsh-graph-runtime
# Or from a local path (e.g. inside the yolo-agent repo)
npm install file:../dsh-graph-runtime

Wiring follows the same steps as any other DSH bundle, either or both:

  • Add one entry to the root composition cordis.yml (this package ships its own cordis.patch.yml; adding the package name to the web profile's dsh.profile.bundles auto-mounts it the same way):

    - id: graph-runtime
      name: dsh-graph-runtime
    

Usage

Graph authors only need defineGraph — defining is registering; no mount or service call is required:

// graphs/echo.ts
import { defineGraph } from 'dsh-graph-runtime'
import { Annotation, END, START, StateGraph } from '@langchain/langgraph'

const State = Annotation.Root({
  topic: Annotation<string>,
  log: Annotation<string[]>({
    reducer: (left, right) => [...left, ...right],
    default: () => [],
  }),
})

export const echoGraph = defineGraph({
  name: 'echo_graph',
  description: 'Echo the given topic.',
  build: () =>
    new StateGraph(State)
      .addNode('echo', async (state) => ({ log: [`seen:${state.topic}`] }))
      .addEdge(START, 'echo')
      .addEdge('echo', END),
  asTool: true, // Declared as a model-visible DSH tool; default false
  // asGraphTool: true, // Also listed in the graphTools registry for discovery; default true, can be omitted
  // validate: (input) => Boolean((input as { topic?: string }).topic), // Argument-validation hook; defaults to always passing
})

The only wiring requirement: a graph module must be imported by some bundle entry (a single barrel line import './graphs' suffices). At plugin apply, every already-defined graph auto-mounts, and later definitions mount immediately; a single graph failing to mount (duplicate name, invalid declaration) warns and is skipped without blocking startup.

Tools declared asTool: true register at the plugin's global layer: visible to every agent (including the default agent). For scope-private or runtime-constructed cases, use the imperative entries below.

GraphRoutingAgent

Trimmed from dsh's ReactLoopAgent to a single routing step: one routing round = the LLM picks a tool → validation → (optional retry) → the graph executes.

import { GraphRoutingAgent } from 'dsh-graph-runtime'

const router = new GraphRoutingAgent(ctx, {
  provider: 'deepseek',
  model: 'deepseek-chat',
  filter: { allow: ['echo_graph', 'search_graph'] }, // allow/deny, same as tools.restrict
  maxRetries: 3, // Maximum LLM re-calls after a failed validation; default 3
  // fallback: myFallbackTool, // Optional: overrides the built-in fallback tool
})

const outcome = await router.route({ input: 'echo hello for me' })
// outcome: { tool: 'echo_graph', args: {...}, result: <the graph's final state> }

Behavior details:

  • A tool must be chosen: dsh-llm's GenerateOptions has no toolChoice field, so the "required" semantics are enforced by the loop — when the model picks no tool (a plain-text reply) there is no retry; the fallback tool runs instead and the outcome is marked fallback: true. The default fallback is the built-in graph_routing_fallback (a no-op: empty input, returns {}); override it through fallback with a ToolDefinition or a GraphDefinition, and the overrider is responsible for keeping empty input {} valid under its schema (e.g. parameters: {}).
  • The validation chain (any failure retries with the reason): the tool exists in the (filtered) registry; the arguments are valid JSON; execution-time schema validation (ToolArgsError); the author's validate hook (GraphValidationError, with context.request available during routing for semantic rejection). A graph's own runtime failure is not a routing failure and propagates as-is.
  • Filtering: filter follows the same allow/deny semantics as tools.restrict (both may be given; they intersect); a list naming unknown tools or an empty result after filtering is a configuration error.
  • Exhausted retries end with the last failure reason.

Routing extension points

Routed executions do not pass through dsh's ctx.tools pipeline (the registry there is a separate surface), so the pipeline-shaped extension points live on the routing loop itself as cordis waterfall events: every chosen-tool attempt traverses graphTools/pre-execute before the graph runs and graphTools/post-execute after it settles. Audit logging, metrics, policy gating, and result transforms all hang off them:

// Gate before execution: a deny feeds the reason back to the model for a
// retry, exactly like a validate rejection.
ctx.on('graphTools/pre-execute', async (execution, next) => {
  if (disabledTools.has(execution.tool)) {
    return { kind: 'deny', reason: `"${execution.tool}" is disabled by policy` }
  }
  return next()
})

// Observe or reshape the settled outcome; fires on successes and failures alike.
ctx.on('graphTools/post-execute', async (execution, outcome, next) => {
  if (execution.tool === 'search_graph' && !('error' in outcome)) {
    return { kind: 'accept', result: redact(outcome.result) } // replace the returned value
  }
  return next()
})

Semantics:

  • execution carries tool / args / request (the original routing text) / signal; the same object reaches both events of one attempt.
  • Waterfall order: listeners run in registration order; next() delegates, and answering without next() vetoes the rest — the first decision wins.
  • deny (pre) and block (post) join the ordinary retry loop: the reason is fed back to the model and retries exhaust into the standard error. A block rejects any settled outcome — it can also convert a propagating graph error into a retried choice instead of an error.
  • accept keeps the settled outcome; with result it replaces the value returned to the caller (listeners still observed the original).
  • The fallback path (no tool chosen) traverses neither event; if you need hooks there, wrap your fallback tool's execute. A throwing listener propagates as-is. Listeners register with plain ctx.on like any cordis event; inside a listener, this is the agent's ctx.

Registration declaration

Field Meaning
asTool Whether to register as a model-visible DSH tool (ctx.tools). Default false; the auto-mount path registers at the plugin's global layer (visible to all agents), the imperative mount path registers at the layer of the ctx passed in.
asGraphTool Whether to list in the graphTools registry for discovery (get/list, GraphRoutingAgent). Default true.

defineGraph fields

Field Meaning
name The graph name, which becomes the registered tool name; globally unique; the reserved name run_code is rejected.
description Optional. The model-facing tool description; defaults to the description written by compile({ description }) on the build() product (when the author compiled it), or a structural description synthesized from nodes and state keys.
build Graph factory: returns an uncompiled builder (the plugin plain-compile()s it at first invocation, passing no options), or an author-compiled instance. Checkpointer and all compile options belong entirely to the author — for state memory, return builder.compile({ checkpointer: new MemorySaver() }).
validate Optional argument-validation hook: receives (input, context) before the graph runs; context.tool is the tool name, and when routed through GraphRoutingAgent context.request carries the original request text (absent on direct calls). Returning false or throwing rejects (a thrown message becomes the rejection reason); defaults to always passing. This enables "this request is not mine" semantic rejection, which the routing loop retries with the reason.
parameters Optional DSH ParameterSchemaSpec. When provided, the whole argument object becomes the graph input; when omitted, the tool exposes a single required input JSON parameter (its description is appended with the discovered state key names, but the value itself stays opaque). Model-visible graphs should always declare it — see the next section.
threadId Optional thread key; forwarded as-is into the graph config's thread_id. Whether it produces state memory is decided by the graph's own checkpointer (the plugin manages none).
configurable Optional extra configurable keys forwarded as-is (thread_id is governed by threadId).
timeoutMs Optional cooperative timeout budget; the graph must respond to the cancellation signal and converge.

Parameter schema quality (required reading for model-visible graphs)

parameters is forwarded as-is as a DSH ParameterSchemaSpec and enforced before execution (out-of-range enums, type mismatches, and missing requireds are all stopped by ToolArgsError). For graphs a model will see, keep every parameter explicitly typed, concretely described, and fully enumerated:

defineGraph({
  name: 'greet_graph',
  build: () => buildGreetGraph(),
  parameters: {
    name: { type: 'string', required: true, description: 'The name to greet.' },
    style: {
      type: 'string',
      required: true,
      enum: ['formal', 'casual'],
      description: 'Greeting style.',
    },
    times: { type: 'integer', description: 'Repeat count.', default: 1 },
    tags: { type: 'array', items: { type: 'string' }, description: 'Extra tags.' },
  },
})

Supported capabilities: type is one of string / number / integer / boolean / null / array / object / json, unions use oneOf; every key may carry description / title / default / examples; enums use an enum matching the type (e.g. a string array), single-value constraints use const; nested objects use object + properties + additionalProperties, arrays use items; mark required keys individually with required: true.

Why nothing is derived automatically: langgraph drops Annotation type information at compile time, leaving only key names and aggregation semantics in the runtime structure — a schema with fabricated types or requireds would mislead the model into assembling arguments the validation layer then rejects, which is worse than an honest input. So the fallback without declared parameters is a single required input (json), whose description carries the discovered state keys (e.g. Expected state keys: topic, log (accumulated).); when asTool: true and no parameters are declared, the mount path logs a warn through ctx.logger to nudge the author.

Imperative entries (dynamic / low-level scenarios)

// A runtime-constructed graph: createGraphDefinition constructs without
// publishing (defineGraph auto-mounts, so an explicit mount while the
// plugin is active would double-register), paired with an explicit mount;
// ctx is both the fiber owner and the ctx.tools registration target.
const dynamicGraph = createGraphDefinition({
  name: 'dynamic_flow',
  build: () => buildDynamicGraph(),
  asTool: true,
})
const mounted = ctx.graphTools.mount(dynamicGraph, ctx)

// Discovery side: ctx.graphTools.get('echo_graph') / ctx.graphTools.list()

// Already holding a compiled graph, or only want a one-off conversion:
const entry = ctx.graphTools.register(compiledGraph, ctx)
const tool = ctx.graphTools.create(compiledGraph)

import { createGraphTool } from 'dsh-graph-runtime' also does the pure conversion directly, with the same effect. Passing a graph definition to register fails with an error pointing at mount — graph definitions carry their own registration declaration. defineGraph targets graph authors (declare-and-publish, auto-mount); createGraphDefinition targets runtime-dynamic scenarios (construction stays construction, mounting stays mounting).

Behavior contract

  • Defining is registering: defineGraph publishes the graph definition to a package-level static queue; at plugin apply the queue is taken over and drained, and later definitions mount immediately. A single graph failing to mount (duplicate name, invalid declaration) is skipped with a ctx.logger warning and never blocks startup; after the plugin disposes, defineGraph queues again and can remount with the next plugin load.
  • Registration is only registration: defineGraph/mount/register never call langgraph's compile(); an uncompiled builder is plain-compile()d exactly once at the tool's first real invocation and memoized (structural discovery only reads the builder's nodes/channels and likewise never compiles).
  • The checkpointer belongs to the graph: the plugin never injects or manages compile options; threadId is only a thread_id pass-through, and whether memory takes effect depends on the instance the author returns from build().
  • Each tool call is one graph.invoke; tool arguments pass DSH parameter-schema validation and the author's validate hook before entering the graph, and the caller's AbortSignal is forwarded as-is.
  • The graph's final state must be lossless JSON, or it is rejected with an explicit error; the tool result returns to the model as a pretty-printed JSON text block.
  • Interaction with ctx.tools happens only when asTool: true is declared: the auto-mount path registers at the plugin's global layer (visible to every agent including the default agent); imperative mount(definition, ctx) registers at the layer of the passed ctx (on an agent-scoped ctx, visible only to that agent, shadowing a same-named global tool). The graphTools registry is separate state: listed names are unique, unregister is idempotent, and entries leave automatically with the disposing fiber of the mounting path.
  • Limits of auto-discovery: Annotation type information and addNode descriptions are dropped when langgraph compiles, so a parameter schema cannot be derived automatically. Discovery degrades safely for a custom GraphInvocable missing runtime fields, but the name must be provided explicitly.
—/ 5

No ratings yet

Verified DSH bundle

Commit 86bb2f84c9c6

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout