- 驱动/技能执行/: oneshot + system 解释器. 读自带 技能/*.json -> 按 tier 选模型 -> 拼 prompt -> 调 OpenAI 兼容端点 -> 剥 JSON -> 逐条按档校验 -> 落盘 + 写 events main 档只锁外层 (plan 是数组 + 每步有 want); small 档锁死 (单值 want + 必须在契约 清单里 + args 键白名单 + confidence 只许 0/0.5/1) - 契约清单只读 PG 的 drivers 表拿 (不 import 别的驱动、不翻别人的文件夹); 也支持 EFI_SKILL_CONTRACTS 显式给一份 (手工跑 / PG 不在时) - 思考型模型把 max_tokens 吃光时, 把预算翻倍重试一次; 本机端点强制不走代理 - 驱动/Json解码: 补 provides ["Json解码:解析"] -- skill 样例里一直在用这个契约名, 之前没人提供 - 文档同步: README / 文档 02 / 文档 06 / 文档 10 (坑 27~30) / 设计 01 / 设计 05 (补记执行侧) - .gitignore: 盖住 驱动/*/输出/ (运行时产物不进 git)
10 KiB
02 · 写一个驱动
驱动 = 一个文件夹。内核认不认它只看一件事:根目录有没有
配置.efi.json。 字段表原文在设计/01-驱动规范.md第 3 节(那份是规范,这份是动手做)。本机已有五个真样板:驱动/Json解码(oneshot)、驱动/样板常驻(resident + 提供服务)、驱动/样例消费器(resident + 消费服务)、驱动/样板技能(带 skill,只列不执行)、驱动/技能执行(示例驱动:把 skill 真跑一遍)。
1. 五分钟:一个最小驱动(oneshot)
cd ~/桌面/工作区/内核
mkdir -p 驱动/问好
驱动/问好/配置.efi.json:
{
"efi": 1,
"name": "问好",
"runtime": "python",
"mode": "oneshot",
"entry": "问好.py",
"interpreter": "system",
"note": "最小驱动: 打印一句话就退"
}
驱动/问好/问好.py:
#!/usr/bin/env python3
"""最小驱动: 打印一句就退 (oneshot)."""
import os
print("问好: 你好, 我是驱动", os.getpid(), flush=True)
print("问好: 内核给我的库连接串 EFI_DB =", "有" if os.environ.get("EFI_DB") else "没有", flush=True)
跑起来:
./.venv/bin/python 内核/内核.py 扫描 # 认文件夹 + 9 条校验 + 写注册表
./.venv/bin/python 内核/内核.py 启动 问好 # oneshot 跑完就退 = 正常
./.venv/bin/python 内核/内核.py 状态 问好
./.venv/bin/python 内核/内核.py 日志 问好 -n 20
状态 里会显示 已退出(oneshot 跑完就退是正常态,不是失败),日志 里是它那两行。
2. 先选形态(选错会白写)
mode |
什么时候用 | 内核怎么对待它 |
|---|---|---|
oneshot |
一次性任务:解码一个文件、跑一趟抓取、生成一份报告 | 起 → 等它自己退;退出码 0 = 正常;不会被"重拉" |
resident |
长跑能力:提供契约、守着一个端口、定时干活 | 起 → 判定为常驻;崩了按 restart 策略决定要不要重拉 |
runtime |
文件夹里放什么 | 内核怎么起 |
|---|---|---|
python |
.py +(可选).venv/ |
<解释器> <entry>,cwd= 驱动根目录 |
exec |
可执行文件(ELF / 脚本,看 x 位 不看后缀) | 直接 exec,内核不找解释器 |
python 的解释器解析顺序(interpreter 字段):显式路径 → venv(.venv/bin/python,不存在回落
system 并记 WARN)→ system(python3)→ 默认 venv。本机样板都用 system,因为样板不需要第三方包。
3. 驱动能碰什么、不能碰什么(硬规矩)
| 表 | 驱动能不能写 | 说明 |
|---|---|---|
events |
✔ 随便写 | 汇报唯一通道(source/driver 写自己名字) |
calls |
✔ 只能插自己的请求 + 回填自己那条的结果 | 要别人的东西走这里 |
drivers / driver_state |
✘ | 内核的地盘(否则驱动能去停别人的进程) |
commands |
✘ | 引导器的地盘 |
环境变量:内核对每个驱动注入 EFI_DB(libpq 关键字式连接串)。密钥类信息走 配置.efi.json 的
env 段或环境变量,不要落盘:
import os, psycopg2
连接 = psycopg2.connect(os.environ["EFI_DB"]) # 连接串由内核给, 驱动不用知道库在哪
连接.autocommit = True
驱动自己 venv 里没装
psycopg2也能跑(只是不能汇报/发请求)—— 内核不会因此判它失败。 样板就是这么写的:拿不到EFI_DB就老老实实跑,不装成功。
4. 汇报:往 events 表插一行
没有协议、没有 socket、没有消息格式 —— 想说什么就插一行(内核只读表,不解析你的 stdout):
def 汇报(连接, message: str, kind: str = "heartbeat") -> None:
"""kind 约定: start|stop|exit|log|produce|heartbeat|error; level: info|warn|error."""
try:
with 连接.cursor() as 游标:
游标.execute(
"INSERT INTO events (source, driver, level, kind, message, data)"
" VALUES (%s, %s, 'info', %s, %s, %s::jsonb)",
("我的驱动名", "我的驱动名", kind, message, json.dumps({"pid": os.getpid()}, ensure_ascii=False)),
)
except Exception as 错: # 汇报失败不该把驱动搞死
print("汇报失败:", 错, flush=True)
看别人汇报:内核/内核.py 事件 -n 50,或按驱动过滤(db.读事件(连接, 条数, driver=名))。
别刷太密 —— events 是总线,刷满了有用的事就看不见了(样板 30s 一条心跳)。
5. 要别人的东西:声明 needs + 往 calls 发请求
你不需要、也不许知道谁提供它。 两步:
① 配置里声明契约名(中立名,不是驱动名):
{
"efi": 1, "name": "样例消费器", "runtime": "python", "mode": "resident",
"entry": "请求.py", "interpreter": "system",
"needs": ["样板:心跳"]
}
② 代码里发请求 → 等结论 → 取结果(完整可跑版见 驱动/样例消费器/请求.py):
# 发起方
游标.execute(
"INSERT INTO calls (caller, want, args, lock_key)"
" VALUES (%s, %s, %s::jsonb, %s) RETURNING id",
("样例消费器", "样板:心跳", json.dumps({"chain": ["样例消费器"]}, ensure_ascii=False), "样板:心跳"),
)
调用id = 游标.fetchone()[0]
# 之后轮询: SELECT state, provider, result, error FROM calls WHERE id = %s
# state 走到 done / failed / denied / timeout 就是有结论了 (等不到要如实说等不到, 别无限等)
被调的一方(resident)自己 LISTEN driver_<我的名字>,内核用 pg_notify 叫醒它:
连接.execute("LISTEN driver_我的驱动名") # 通道名只跟"我是谁"有关, 跟谁在调我无关
# 收到通知 -> payload 是 calls.id -> 干活 -> UPDATE calls SET state='done', result=… WHERE id=…
oneshot 的提供方不用自己 LISTEN:内核仲裁时发现它没在跑,会按需把它拉起来跑一遍。
排障、六条仲裁(越权/成环/无人提供/按需拉起/同锁排队/超时收权)、锁与超时的细节 → 04-契约与调用.md。
6. 常驻驱动的三条纪律
-
收到
SIGTERM要干净退出。装死不退,内核只能SIGKILL(日志/状态都不好看):def 收工(号: int, 帧: object) -> None: print("我的驱动: 收到信号", 号, "收工", flush=True) sys.exit(0) signal.signal(signal.SIGTERM, 收工) signal.signal(signal.SIGINT, 收工) -
stdout 随便打,但要知道它去哪:你的 stdout/stderr 被内核重定向进
驱动/<名>/logs/<名>.log(每次启动前插一条==== 启动 … ====分隔头)。用内核/内核.py 日志 <名> -f实时跟。 -
打点节流:每 5s 打印一行是给人看的(老板 tail 得见它在活着),
events30s 一条就够。
7. 调试手法(按这个顺序查最快)
| 想确认 | 怎么做 |
|---|---|
| 文件夹被认了没 | 内核/内核.py 列表;无效 的话看 说明 列 / 状态 <名> 的"上次错误" |
| 内核怎么判它的 | 内核/内核.py 状态 问好 --json(含 PID / 命令行 / 配置是否待重启 / 重启次数 / 契约) |
| 它说了什么 | 内核/内核.py 日志 <名> -n 50(-f 实时跟) |
| 内核怎么记账的 | 内核/内核.py 事件 -n 50;内核/内核.py 日志 --内核 |
| 不起眼但最有用的一招 | 直接手动跑一遍:python3 驱动/问好/问好.py(驱动是普通程序,不需要内核就能跑) |
| 配置改完不生效 | 配置有 config_hash 指纹,改完要重启:内核/内核.py 重启 <名>(列表 会提示"待重启") |
8. 驱动可以带 skill(归你管)
驱动不只会跑代码,还可以携带 skill —— 声明式文件放在 驱动/<名>/技能/*.json,一个 skill 一个文件。
规矩只有一条:内核不知道 skill 的存在,读它、解释它、决定给哪个模型,全是驱动自己的事
(内核只管确定性代码能力的接入与仲裁,一旦它掺和 skill,"模型"这个概念就漏进内核了)。
同一件事写两份,分模型档(字段表与两档差别见 设计/05-技能规范.md):
| 档 | 给谁 | 写法 |
|---|---|---|
tier: "main" |
云端强模型 / 本地 27B+ | 步骤抽象 3 条,只锁外层结构,规划权交给模型 |
tier: "small" |
本地 4B~9B | 步骤写死编号 5 条,字段白名单 + 置信度只许三档,含反例 few-shot |
最小骨架(完整样例看 驱动/样板技能/技能/请求解析-主模型.json):
{ "efi": 1, "name": "…", "version": "0.1", "tier": "main",
"description": "一句话", "when": ["什么时候用"],
"input": { "说明": "…", "schema": {} }, "steps": ["…"],
"output": { "format": "json", "schema": {}, "说明": "…" },
"examples": [ { "输入": {}, "输出": {} } ], "boundaries": ["不许编 / 不许猜"], "notes": ["给谁跑"] }
驱动怎么用:启动时读自己目录下的 技能/,按 tier 选模型、拼 prompt、校验输出、失败重试 —— 全在你手里。
驱动/样板技能/技能.py 是最小示范(只把自带的 skill 列出来打印,不执行)。
要真跑(按档选模型 → 拼 prompt → 调模型 → 剥 JSON → 逐条校验输出 → 落盘 + 写 events)看
驱动/技能执行/执行.py —— 它把同一份 small 档 skill 真喂给本地模型跑了一遍,两个反例(多步请求、信息不足)
都会按 skill 写的规矩回问而不是硬猜。
9. 交付一个驱动前的检查表
- 文件夹根目录有
配置.efi.json,且efi=1/name唯一 /runtime/entry齐全 entry是相对路径,不出驱动根(不许..)mode选对(跑完就退的用oneshot,长跑用resident)autostart/restart都是想要的(默认false/no—— 手动控制口径)- 要别人的东西写的是契约名(
needs),代码里没出现别的驱动名 - 只写
events/calls;碰不到drivers/driver_state/commands SIGTERM干净退出(resident必须)- 汇报失败、连不上 PG 都有兜底(别把驱动搞死、别装成功)
内核/内核.py 扫描→ 启动 → 状态 → 日志 四步都看过一眼- 敏感信息走
env/ 环境变量,没写进配置.efi.json