Skip to content

dsh-graph-runtime

Verified

dsh-graph-runtime · v0.2.0 · MIT

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

Install

dsh plugin add dsh-graph-runtime

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Creators

Readme

dsh-graph-runtime

English | 中文

DeepSeek Harness(DSH)的 graph 运行时能力集,以独立 Cordis 插件/bundle 形式发布。

当前能力:

  • 图定义与自动挂载:以 defineGraph 定义"扩展 StateGraph"——图工厂 + 注册声明(是否成为 DSH tool、是否登记注册表)+ 工具入参校验钩子。模块被 加载即完成注册:插件启动时自动挂载已定义的图,其后定义的即时挂载,开发者 无需任何 service 调用。工具名/描述自动发现(compile({ name, description }) 或结构合成),编译惰性且记忆化(注册路径零编译副作用),参数校验、取消转发 与结果渲染都由本插件承担。
  • GraphRoutingAgent:graph 驱动的路由 Agent。把 graphTools 注册表(可 过滤)交给 LLM,要求必须选择一个工具;对返回的工具调用做存在性、JSON、 schema 与作者 validate 校验,不通过带原因重试(默认 3 次,可配置), 通过后执行对应 graph。每次被选中工具的执行前后有 graphTools/pre-execute / graphTools/post-execute 两个 waterfall 扩展点(门禁、审计、结果加工)。

安装与接入

# 发布后
npm install dsh-graph-runtime
# 或本地路径(例如在 yolo-agent 仓库内)
npm install file:../dsh-graph-runtime

接入方式与其他 DSH bundle 一致,二选一或同时:

  • 根组合 cordis.yml 增加一行(本包自带 cordis.patch.yml,web profile 的 dsh.profile.bundles 加入包名后同样自动挂载):

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

用法

图作者只需 defineGraph——定义即注册,不需要任何 mount 或 service 调用:

// 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, // 声明为模型可见的 DSH tool;默认 false
  // asGraphTool: true, // 登记进 graphTools 注册表供发现;默认 true,可省
  // validate: (input) => Boolean((input as { topic?: string }).topic), // 入参校验钩子,缺省恒通过
})

唯一的接线要求:graph 模块需要在某个 bundle 入口被 import(barrel 一行 import './graphs' 即可)。插件 apply 时自动挂载已定义的全部图,其后 定义的即时挂载;单个图挂载失败(重名、非法声明)告警跳过,不阻断启动。

asTool: true 的工具注册在插件的全局层:所有 agent(含默认 agent)可见。 需要 scope 私有或运行时动态构造的场景,使用下述命令式入口。

GraphRoutingAgent

参考 dsh ReactLoopAgent 裁剪为单步:一次路由 = LLM 选工具 → 校验 → (可选重试)→ 执行 graph。

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

const router = new GraphRoutingAgent(ctx, {
  provider: 'deepseek',
  model: 'deepseek-chat',
  filter: { allow: ['echo_graph', 'search_graph'] }, // 同 tools.restrict 的 allow/deny
  maxRetries: 3, // 校验不通过时重新调用 LLM 的最大次数,默认 3
  // fallback: myFallbackTool, // 可选:覆盖内置兜底 tool
})

const outcome = await router.route({ input: '帮我回显一下 hello' })
// outcome: { tool: 'echo_graph', args: {...}, result: <graph 最终 state> }

行为细节:

  • 必须选工具:dsh-llm 的 GenerateOptions 没有 toolChoice 字段, "required" 语义由循环兜底——模型未选择任何工具(纯文本回复)时不重试, 直接执行兜底 tool 并在结果上标记 fallback: true。缺省兜底是内置的 graph_routing_fallback(什么都不做:空入参、返回 {});可通过 fallback 传入 ToolDefinitionGraphDefinition 覆盖,覆盖者自行 保证空入参 {} 能通过其 schema(如 parameters: {})。
  • 校验链(任一不通过即带原因重试):工具存在于(过滤后的)注册表; 参数是合法 JSON;执行期 schema 校验(ToolArgsError);作者 validate 钩子(GraphValidationError,经路由时携带 context.request 可做语义 拒绝)。graph 自身的运行失败不是路由失败,原样上抛。
  • 过滤filter 的 allow/deny 语义同 tools.restrict(可同给取交集); 名单含未知工具或过滤后为空视为配置错误。
  • 重试用尽后以最后一次失败原因收场。

路由扩展点

被路由的执行不走 dsh 的 ctx.tools 管线(那是另一套注册面),管线 形态的扩展点因此落在路由循环自身:以 cordis waterfall 事件提供,每个 被选中工具的尝试在 graph 执行前经过 graphTools/pre-execute、在执行 落定后经过 graphTools/post-execute。审计日志、指标、策略门禁、结果 加工都挂在这两个事件上:

// 执行前门禁:deny 的原因会反馈给模型重试,与 validate 拒绝完全一致。
ctx.on('graphTools/pre-execute', async (execution, next) => {
  if (disabledTools.has(execution.tool)) {
    return { kind: 'deny', reason: `"${execution.tool}" 已被策略禁用` }
  }
  return next()
})

// 观察或改写落定结果;成功与失败都会经过。
ctx.on('graphTools/post-execute', async (execution, outcome, next) => {
  if (execution.tool === 'search_graph' && !('error' in outcome)) {
    return { kind: 'accept', result: redact(outcome.result) } // 替换返回值
  }
  return next()
})

语义:

  • execution 携带 tool / args / request(原始路由文本)/ signal;同一次尝试的两个事件收到同一个 execution 对象。
  • waterfall 顺序:监听器按注册顺序执行;next() 表示放行并委托后续, 不调 next() 直接作答则否决余下监听器——第一个决策生效。
  • deny(pre)与 block(post)并入常规重试循环:原因反馈给模型, 重试用尽后抛标准错误。block 拒绝的是任何已落定的结果——也可以把 本会上抛的 graph 错误转化为一次带原因的重试。
  • accept 维持落定结果;携带 result 时替换返回给调用方的值(各监听 器观察到的仍是原值)。
  • 兜底路径(模型未选工具)不经过这两个事件;如需在兜底上加钩子,请 自行包装 fallback 工具的 execute。监听器抛错会原样上抛。监听器用 普通 ctx.on 注册(与任何 cordis 事件一致);监听器内 this 是 Agent 所在的 ctx。

注册声明

字段 说明
asTool 是否注册为模型可见的 DSH tool(ctx.tools)。默认 false;自动挂载路径注册在插件全局层(所有 agent 可见),命令式 mount 路径注册在传入 ctx 的层。
asGraphTool 是否登记进 graphTools 注册表供发现(get/list、GraphRoutingAgent)。默认 true

defineGraph 字段

字段 说明
name 图名,即注册后的 tool 名;全局唯一,禁用保留名 run_code
description 可选。面向模型的工具描述;缺省时取 build() 产物上 compile({ description }) 写入的描述(作者自行编译时),或由节点与 state 键合成的结构描述。
build 图工厂:返回未编译 builder(插件在首次调用时 plain compile(),不传任何选项),或作者自行编译的实例。checkpointer 等编译选项完全归作者所有——要 state 记忆就返回 builder.compile({ checkpointer: new MemorySaver() })
validate 可选的工具入参校验钩子:graph 执行前收到 (input, context)context.tool 是工具名,经 GraphRoutingAgent 路由时 context.request 携带原始请求文本(直连调用时无此字段)。返回 false 或抛错即拒绝(抛错 message 进入拒绝原因);缺省恒通过。据此可实现"这个请求不属于我"的语义拒绝,路由循环会带原因重试。
parameters 可选的 DSH ParameterSchemaSpec。提供时整个参数对象作为 graph 输入;缺省时工具暴露唯一的必填 input JSON 参数(其描述会拼上结构发现的 state 键名,但值本身不透明)。模型可见的图应始终声明,见下节。
threadId 可选的线程键;仅透传为 graph config 的 thread_id。是否形成 state 记忆由 graph 自带的 checkpointer 决定(插件不代管)。
configurable 可选的 configurable 附加键透传(thread_idthreadId 为准)。
timeoutMs 可选的协作式超时预算;graph 必须响应取消信号并收敛。

参数 schema 质量(模型可见的图必读)

parameters 原样透传为 DSH ParameterSchemaSpec 并在执行前强制校验 (越界枚举、类型不符、缺 required 都会被 ToolArgsError 拦下)。给模型 看的图,请保证每个参数类型明确、描述具体、枚举列全

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.' },
  },
})

支持的能力:typestring / number / integer / boolean / null / array / object / json,联合用 oneOf;每个键可带 description / title / default / examples;枚举用与类型匹配的 enum(如字符串数组),单值约束用 const; 嵌套对象用 object + properties + additionalProperties,数组用 items; 逐键 required: true 标注必填。

为什么不自动派生:langgraph 在编译时即丢弃 Annotation 的类型信息, 运行时结构只剩键名与聚合语义——伪造类型或 required 的 schema 会误导模型 拼出被校验层拒绝的参数,比诚实的 input 更糟。因此未声明 parameters 的兜底是单一必填 input(json),结构发现的 state 键会拼进它的描述 (如 Expected state keys: topic, log (accumulated).);asTool: true 且未声明参数时,挂载路径会经 ctx.logger 打一条 warn 提醒作者。

命令式入口(动态/低层场景)

// 运行时动态构造的图:createGraphDefinition 只构造不发布(defineGraph
// 会自动挂载,插件活跃时再显式 mount 会双重注册),配合显式 mount;
// ctx 是 fiber 归属与 tools 注册落点。
const dynamicGraph = createGraphDefinition({
  name: 'dynamic_flow',
  build: () => buildDynamicGraph(),
  asTool: true,
})
const mounted = ctx.graphTools.mount(dynamicGraph, ctx)

// 发现侧:ctx.graphTools.get('echo_graph') / ctx.graphTools.list()

// 手里已是编译产物、或只想一次性转换:
const entry = ctx.graphTools.register(compiledGraph, ctx)
const tool = ctx.graphTools.create(compiledGraph)

也可以直接 import { createGraphTool } from 'dsh-graph-runtime' 做纯转换, 效果等价。向 register 传图定义会得到指向 mount 的错误——图定义自带 注册声明。defineGraph 面向图作者(声明即发布、自动挂载); createGraphDefinition 面向运行时动态场景(构造归构造,挂载归挂载)。

行为契约

  • 定义即注册:defineGraph 把图定义发布到包级静态队列;插件 apply 时 接管并清空积压,其后定义的图即时挂载。单个图挂载失败(重名、非法声明) 经 ctx.logger 告警跳过,不阻断启动;插件释放后 defineGraph 重新入队, 可随插件重载再次挂载。
  • 注册只是注册:defineGraph/mount/register 全程不调用 langgraph 的 compile();未编译 builder 在该 tool 首次实际调用时才 plain compile() 一次并记忆化(结构发现只读取 builder 的 nodes/channels,同样不触发编译)。
  • checkpointer 归 graph:插件从不注入或代管编译选项;threadId 仅是 thread_id 透传,记忆是否生效取决于作者在 build() 里返回的实例。
  • 每次工具调用即一次 graph.invoke;工具入参先经 DSH 参数 schema 校验与 作者 validate 钩子,再进入 graph;调用方的 AbortSignal 原样转发。
  • graph 最终 state 必须是 lossless JSON,否则以明确错误拒绝;工具结果以 pretty JSON 文本块返回给模型。
  • ctx.tools 的交互只发生在声明 asTool: true 时:自动挂载路径注册在 插件的全局层(所有 agent 含默认 agent 可见);命令式 mount(definition, ctx) 注册在传入 ctx 的层(agent scoped ctx 上则仅该 agent 可见并 shadow 全局同名工具)。graphTools 注册表是独立状态:登记名唯一、unregister 幂等、随挂载路径的 fiber 释放自动移出。
  • 自动发现的边界:Annotation 的类型信息与 addNodedescription 在 langgraph 编译时即被丢弃,参数 schema 无法自动派生。自定义 GraphInvocable 缺少运行时字段时发现安全降级,但名字必须显式提供。