jev-dsh — 第一层:把 System One(TypeSafe Jev)接成 DSH 的决策循环
给 DSH 一个 system_one 工具 + 一个技能:agent 可以对已有上下文做校准过的有界决策
(choice / noul / score),而不是把 Jev 当聊天模型塞进模型选择器。
已经可以用了,而且离线可验证(不需要真 API key)。
为什么是工具而不是模型
Jev 是 决策模型,不是 chat 模型:线协议只有 POST /v1/systemone,输入 state + 类型化
questions,输出带概率的 answers;不生成文本、不流式、不调工具。实测
/v1/chat/completions → 404、/v1/responses → 404、/v1/systemone → 403(存在、缺 key)。
DSH 的 llm-pi-ai 只认三种 chat 协议,所以这条路本来就不通(详见 research.md)。
生态里的正确形态就是"工具 + 技能"——pi-system-one 对 Pi 就是这么做的,本实现复用了它的
客户端与技能文本,只重写了 Pi 专属的外壳。
快速开始
# 装进默认 profile($DSH_HOME/profiles/web)
node install.mjs # 只写行,设置全部留给环境变量
node install.mjs --base-url https://api.typesafe.ai --model jev-latest
node install.mjs --anonymous # 本地 Reflex:http://localhost:8008,不带凭据
# 只看会写什么 / 卸载
node install.mjs --print
node install.mjs --uninstall
install.mjs 往 profile 的 cordis.patch.yml 写一个受管块:工具行(绝对路径,因为
out-of-tree 插件不是按包名加载的)+ 一条 skill-filesystem 的 customSkillDirs 指向本包的
skills/。该文件被 hmr 行热监听,运行中的 profile 会自行重组,无需重启。
带任一设置参数时,安装器会明确写出 baseUrl: https://api.typesafe.ai、model: jev-latest、
apiKeyEnv: TYPESAFE_API_KEY;不带参数则一个字段都不写。
命名与凭据(都用 TypeSafe 自己的名字)
环境变量:TYPESAFE_BASE_URL、TYPESAFE_MODEL、TYPESAFE_API_KEY、TYPESAFE_TIMEOUT_MS、
TYPESAFE_PATH、TYPESAFE_API_KEY_ENV。这样已经有 TypeSafe SDK 变量的部署不用再导一套。
| 规则 | 说明 |
|---|---|
| 默认凭据引用 | TYPESAFE_API_KEY;行里的 apiKeyEnv 或环境变量 TYPESAFE_API_KEY_ENV 可以换成别的引用 |
| 存储位置 | $DSH_HOME/.credentials.yaml 的 refs:——就是 Models 页 credentials.set(ref, value) 写的那份文件,改了热生效、不用重启 |
| 查找顺序 | 先 ctx.credentials.resolve(ref)(其内部优先级是"启动环境 > 存储文件 > .env"),进程环境兜底 |
| 未点名 + 解析不到 | 不报错,不带凭据发请求:本地 Reflex 就该这么用,端点自己会回答它要不要 key |
| 点名 + 解析不到 | 明确报错并指出引用名,不会静默发匿名请求(与 llm-pi-ai 对 apiKeyEnv 的处理一致) |
最小配置:只把密钥写进凭据文件即可,不用改 profile。
# $DSH_HOME/.credentials.yaml
version: 1
refs:
TYPESAFE_API_KEY: sk-…
注意:
install.mjs默认写的是真实 web profile。只想试的话用DSH_PROFILE_DIR=<某测试 profile 目录> node install.mjs。 另外,hmr行的root列表会被后安装的插件覆盖(手机插件也这么写, 它是另一个仓库),两个都装时只有最后安装的那个能热重载源码。
工具怎么用
{
"state": "工单:我的打款连续三天失败,没人回复。客户等级:growth。",
"questions": {
"team": { "type": "choice", "instructions": "哪个团队应该接手?",
"criteria": { "billing": "打款、发票、退款", "technical": "缺陷、故障、集成" } },
"urgent": { "type": "noul", "instructions": "这表达了紧急吗?" },
"frustration": { "type": "score", "instructions": "客户有多烦躁?",
"criteria": ["平静", "烦躁", "非常生气"] }
}
}
宽容的入参(会修复表示,修不了的报错点名到字段):
| 输入 | 处理 |
|---|---|
context / evidence |
state 的别名;同时给两个会被拒(只有调用方知道哪个是本意) |
pick / select / yesno / rating / scale |
类型别名 → choice / noul / score |
省略 type |
按 criteria 形状推断:数组→score,对象→choice,无→noul |
question / prompt / ask;options / levels / labels |
instructions / criteria 的别名 |
"id": "问题文本" |
裸字符串 → noul |
数组形式 [{ "id": "team", ... }] |
与 map 形式等价 |
criteria: ["a","b"](choice) |
展开为 {a: null, b: null} |
criteria: ["是","否"](noul) |
展开为 {true: "是", false: "否"} |
拒绝并给出可操作信息的情形:缺 state、同时给两个 state 别名、缺/空 questions、未知类型、 choice 没有 criteria、score 少于两级或多于十级、choice 超过 255 项、缺 instructions、 重复 id、数组形式缺 id、state 超过 128KB(提示裁剪,服务端上限 32k token)。
返回给模型的是分布而不只是赢家:
system-one · jev-1.13.0 · 5ms · in 128 / out 16 tok
[choice] team → billing (confidence 0.7)
billing 0.7 · technical 0.3
[noul] urgent → 0.9
[score] frustration → 1 (confidence 0.8)
Calm 0.1 · Frustrated 0.8 · Very angry 0.1
验证(全部离线可跑)
| 层 | 命令 | 结果 |
|---|---|---|
| 入参修复/拒绝 | node test/normalize.test.mjs |
30 项断言通过 |
| 渲染 | node test/render.test.mjs |
6 项通过 |
| 工具 schema 过 harness 真校验器 | ./test/schema.test.sh |
13 项通过:parameters/output 被 assertSupportedJsonSchema 接受;TYPESAFE_ 命名、环境变量回退、以及 install.mjs --print 写出的 apiKeyEnv: TYPESAFE_API_KEY / --anonymous 形态都与运行时一致 |
| 真实 HTTP 执行(含错误路径) | node test/execute.e2e.mjs |
19 项通过:自起 mock/mock-systemone.mjs,断言线上 body、Bearer 头、403、未点名凭据匿名发请求、点名凭据缺失报错、未配置端点、TYPESAFE_ 环境变量 |
| 决策循环驱动手机 | node test/adapter.e2e.mjs |
14 项通过:观察步不打扰模型、refs 永远来自最新一屏、重复 tap 被挡下 |
| 长候选表(翻译 App 选语言那一类) | node test/adapter-candidates.e2e.mjs |
8 项通过:40 行候选 + 目标概率 0.2 默认照常动手;设了 gate.candidates 之后被拒的是那一行,tap 仍在菜单里 |
| 重放一次录下来的会话 | node test/replay-session.test.mjs |
8 项通过:goal/历史从转录里读出来、世界按转录回答、换了选择报 DIFFERENT、转录没问过的报 unrecorded |
| 决策循环本身 | cd kernel && npm test |
loop.e2e.mjs 九条终止路径 + candidates.test.mjs 20 项(低目标概率、坏分布 fail-closed、候选下限、左中文右英文整条跑通) |
| 真实 DSH 全链路 | 见下 | 插件挂载 → 工具注册(tools=25)→ 模型调用 → 真实 HTTP → 结果以 role:"tool" 回传 → 技能进入会话目录 |
| 凭据库分支(和 OpenAI 同一条路) | 见下 | 密钥只放在 $DSH_HOME/.credentials.yaml 的 refs: { TYPESAFE_API_KEY: … },环境变量不设 → 请求带上正确 Bearer;把该文件移走 → 立即报 no credential for TYPESAFE_API_KEY(反向对照,证明来源就是存储文件) |
上表里驱动手机的两条(
adapter.e2e.mjs、adapter-candidates.e2e.mjs)是唯一跨到另一个仓库 的测试:它们要真的mobile_test_*工具,才能证明"先观察后决策""refs 来自最新一屏"。 本仓库对手机没有运行时依赖——profile 用路径点名capabilities——所以手机不在场时这两条 跳过而不是失败(test/phone-core.mjs按DSH_PHONE_CORE→ 同级的phone-core/或dsh-phone/phone-core/顺序找)。单独 clone 本仓库npm test依然全绿。
全链路那条的做法:测试 profile 里用 install.mjs 装本插件,再用 e2e/llm-overlay.yml 把 agent
自己的模型指向 e2e/mock-llm.mjs(一个只会返回 system_one 工具调用的假 chat 端点),
于是整条链路不需要任何真实 key:
node mock/mock-systemone.mjs & # :8940,带 key 校验
node e2e/mock-llm.mjs & # :8942,会调用 system_one
DSH_PROFILE_DIR=<测试 profile> node install.mjs --base-url http://127.0.0.1:8940 --model jev-latest
# 凭据走存储文件(不设 TYPESAFE_API_KEY 环境变量):
printf 'version: 1\nrefs:\n TYPESAFE_API_KEY: mock-key\n' > <测试 home>/.credentials.yaml
DSH_HOME=<测试 home> MOCK_CHAT_API_KEY=mock-key \
node <checkout>/apps/cli/lib/bin.js --profile headless \
--patch e2e/llm-overlay.yml "Judge this ticket"
# → TOOL-LOOP-OK tools=25 result=system-one · jev-1.13.0 · 5ms | [choice] team → billing …
目录结构
jev-dsh/
├── package.json 插件包(纯 ESM JS,无构建步骤)
├── install.mjs 写/删 profile 里的受管块(--print / --uninstall / --base-url …)
├── src/
│ ├── tools.js 插件入口:system_one 的定义与注册(apply / createToolDefinitions)
│ ├── normalize.js 入参修复与拒绝(纯函数,可单测)
│ ├── client.js 端点与凭据绑定,system-one-core 的 HttpSystemOneProvider
│ ├── credential.js 凭据引用怎么解析(存储文件 / 环境变量)
│ ├── checkout.js DSH checkout 在哪(`DSH_CHECKOUT` → 逐级向上 → 两个惯例位置)
│ └── render.js 答案 → 模型可读文本
├── adapter/ 同一条决策循环,但挂成 harness 的 LlmAdapter(`llm-systemone` 行)
│ 由安装器写行:install.mjs 之外还有 adapter/install.mjs
├── preset/install.mjs 装「Jev」模式(模式 = persona + systemone 路由 + 一个工具行)
├── kernel/ 移植自 SystemOneHarness 的 loop:observe → encode → ask → gate → execute
├── skills/system-one/ SKILL.md + references/use-cases.md(源自 pi-system-one,改了 2 处)
├── tools/store-key.mjs 把密钥写进 $DSH_HOME/.credentials.yaml 的 refs:,不经过 shell 与屏幕
├── mock/mock-systemone.mjs 脚本化 /v1/systemone 端点
├── e2e/ mock-llm.mjs、llm-overlay.yml、测试 profile
├── test/ normalize / render / say / space / decide / adapter / 执行与 schema
├── notes/ research.md(调研与实测)、USAGE.md(当前怎么用)
└── vendor/ 第三方快照:system-one-core(运行时唯一依赖)、pi-system-one
出处与许可见 vendor/README.md
把一次真实会话重放一遍
改门、改菜单、改候选表之后,"这次是不是好点了"不该靠感觉。tools/replay-session.mjs 把一次录下来的
会话按当前代码再问一遍:同一句 goal、同一屏、同一段历史,逐条对照当时做了什么。
# 真端点(凭据取 TYPESAFE_API_KEY 或 $DSH_HOME/.credentials.yaml)
node tools/replay-session.mjs --session 9057753a --turn 2
# 没有 key 时用脚本答案,链路与测试同一套
node tools/replay-session.mjs --session 9057753a --turn 2 --script /tmp/steps.json
# 全部轮次
node tools/replay-session.mjs --session 9057753a --all --steps 8
每步标 same / DIFFERENT / unrecorded,退出码非 0 表示有对不上的地方。三条规矩:
- 世界只从转录来:循环问什么,就用当时录下的那条结果回答,顺序对得上才用;对不上就不编。
- 同名不同 kind 不算"改了选择":把 tap 的结果喂给 launch 会让循环基于一个没发生过的世界推理,
所以那种情况记
unrecorded,而不是拿去对齐。 - 多帧 zstd 必须整份解:DSH 每 flush 一次追加一帧,Node 自带的解码器只解第一帧。截断的转录
比拒绝更危险,所以这里优先用
zstdCLI,只有它缺席且解出来不完整时才明确报错。
几个实现决定
system-one-core是 vendored(vendor/system-one-core)而不是 npm 依赖:out-of-tree 插件 按绝对路径加载、裸模块名会按 profile 目录解析,那里没有包。相对路径导入是确定性的,也就不需要 在宿主机上装任何东西。- 不导出
Configschema:声明 schemastery schema 需要 import@deepseek-ai/schemastery, 同样在 profile 目录不可解析。改用 loader 行config+ 环境变量,行为一致。 - 原始 JSON-Schema 定义而非
defineTool(...):理由同上(@deepseek-ai/dsh-tools不可导入); 入参检查因此由本包自己负责,normalize.js就是那份检查,schema.test.sh借 harness 的校验器 补上类型层面。 - 工具始终注册:端点没配时报一条可操作的错误(
no endpoint configured; set TYPESAFE_BASE_URL …), 比静默不注册更容易发现和排查。 - 门的两个问题分开判:动作的 confidence 与设计者声明的参数按风险阈值取最弱值;
from:参数 (选项由环境这一步枚举)改成判"分布是否自洽 + 选中是否 argmax",绝对下限是gate.candidates(默认 0)。理由与代价写在kernel/README.md:把候选概率折进风险最小值,会让 120 行的语言 选择器永远无法行动——那是"该点哪一个"的问题,不是"要不要点"的问题。 - refusal 缩到出问题的那一层:动作弱就从菜单里拿掉动作(
withoutChoices),目标弱就从名单里 拿掉那一项(withoutCandidate),而不是一律删动作。否则一个控件没把握的代价是这一回合不能再 点任何东西,而屏幕本身并没有变得更难。窄化同时进next_action.criteria的校验,所以被拿掉的 选项即使被重新选中也会被拒("没被展示的就不能选"要能扛住模型无视菜单)。
没做的
pi-system-one的/so config、/judge命令面:那是 Pi 的斜杠命令与 UI API (ExtensionAPI:session_start、resources_discover、ui.notify),DSH 没有对应物。- UHP:
SystemOneHarness(Python)已经是 UHP Core 合规的服务,DSH 侧要做客户端才行,属于另一件事。 - 真模型复测:本机没有
TYPESAFE_API_KEY,也没有本地 Reflex 端点,所以延迟/定价/真答案分布 都还是文档值。拿到 key 后把TYPESAFE_BASE_URL指向真端点跑同一条全链路即可。
No comments yet. Be the first to write one.