dsh-graph-runtime
Verifieddsh-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传入ToolDefinition或GraphDefinition覆盖,覆盖者自行 保证空入参{}能通过其 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_id 以 threadId 为准)。 |
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.' },
},
})
支持的能力:type 取 string / 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 首次实际调用时才 plaincompile()一次并记忆化(结构发现只读取 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的类型信息与addNode的description在 langgraph 编译时即被丢弃,参数 schema 无法自动派生。自定义GraphInvocable缺少运行时字段时发现安全降级,但名字必须显式提供。