Senses trigger shell commands only. Workflows are invoked via CLI.
SenseTrigger is now { command: string } — no discriminated union.
Closes #318
Co-authored-by: Cursor <cursoragent@cursor.com>
17 KiB
name, description, license, compatibility, metadata
| name | description | license | compatibility | metadata | ||||
|---|---|---|---|---|---|---|---|---|
| nerve-dev | Nerve development guide for AI coding agents. Covers architecture (sense → signal → workflow), CLI commands, sense and workflow development patterns, nerve.yaml configuration, coding conventions, and common pitfalls. Use when developing senses, workflows, or contributing to the Nerve codebase. | MIT | Requires Node.js 22+, pnpm, TypeScript. Nerve monorepo with packages/core, packages/cli, packages/daemon. |
|
Nerve 开发指南
Nerve 是一个轻量级本地感知引擎,为自主 agent 持续观察外部状态、响应变化、编排多步骤工作流。
核心数据流
External World → Sense → Signal → Workflow → Log
↑ ↑
"观察什么" "做什么"
| 概念 | 说明 |
|---|---|
| Sense | compute() 函数,采样或派生数据。返回 T | null — 非 null 发出 Signal,null 静默。每个 Sense 有自己的 SQLite 数据库 |
| Signal | Sense 返回非 null 时发出的通知。纯事实,无意图。通过内存 Signal Bus 分发,不持久化 |
| Workflow | 有状态的多步骤执行。包含 Roles(有副作用的执行者)和 Moderator(纯路由器)。每个实例是一个 Thread |
| Log | 不可变审计日志。记录执行、状态变更、错误 |
| Kernel | 编排核心。持有 Signal Bus、调度器、进程管理、Workflow Manager |
| Daemon | nerve-daemon — 后台运行的引擎进程 |
用户工作区 ~/.uncaged-nerve/
~/.uncaged-nerve/
nerve.yaml # 主配置
package.json # 依赖(@uncaged/nerve-core, drizzle-orm 等)
senses/ # sense 模块
cpu-usage/
package.json # esbuild 构建脚本
src/
index.ts # compute 函数(TypeScript 源码)
schema.ts # Drizzle ORM schema
index.js # 构建产物(由 pnpm run build 生成)
migrations/ # SQL 迁移
workflows/ # workflow 模块
alert/
index.ts # WorkflowDefinition
data/ # SQLite 数据库、signal 存储
logs/ # 日志
nerve.yaml 配置
senses:
cpu-usage:
group: system # worker 分组(同组 sense 共享一个 worker 进程)
throttle: 5s # 最小计算间隔
timeout: 10s # compute 超时
grace_period: null # 首次错误后的宽限期
retention: 10000 # _signals 表最大行数
interval: 10s # 定时轮询周期
on: [disk-usage] # 响应其他 sense 的 signal 触发计算
disk-usage:
group: system
throttle: 30s
workflows:
alert:
concurrency: 1 # 最大并发 thread 数
overflow: queue # 超出时排队(或 drop)
max_rounds: 10 # workflow 默认最大轮数
api: # HTTP API 配置
bind: "127.0.0.1:9800"
token: null # loopback 不强制 token
调度方式:
interval: 10s— 每 10 秒触发一次 computeon: [other-sense]— 当 other-sense 发出 signal 时触发- 两者可组合
Sense 开发
Sense Anatomy
一个 sense 由以下文件组成(nerve create sense <name> 自动生成):
senses/<name>/
package.json # esbuild 构建脚本 + devDependencies
src/
index.ts # compute() 函数(TypeScript 源码)— 必须
schema.ts # Drizzle ORM 表定义 — 推荐
index.js # 构建产物(daemon 实际加载此文件)— 由 pnpm run build 生成
migrations/ # SQL 迁移文件
0001_init.sql # 建表 SQL — Drizzle Kit 生成
命名规范:sense ID 用 kebab-case(如 cpu-usage),对应 SQL 表名自动转 snake_case(cpu_usage)。
构建流程:nerve create sense <name> 脚手架后会自动运行 pnpm install && pnpm run build,将 src/index.ts 打包为 index.js。修改源码后需手动重新构建:
cd ~/.uncaged-nerve/senses/<name>
pnpm run build
compute 函数
compute 返回 null(静默)或 { signal, workflow } 结构:
// senses/cpu-usage/src/index.ts
import type { LibSQLDatabase } from "drizzle-orm/libsql";
import { cpuUsage } from "./schema.js";
type SenseResult = {
signal: { cpu: number; ts: number };
workflow: null;
} | null;
export async function compute(
db: LibSQLDatabase,
_peers: Record<string, LibSQLDatabase>,
_options: { signal: AbortSignal },
): Promise<SenseResult> {
void cpuUsage;
// db: Drizzle ORM SQLite 实例(读写,当前 sense 专用)
// _peers: Record<string, DrizzleDB>(只读,同组其他 sense 的 DB)
// _options: { signal: AbortSignal }
const usage = getCpuUsage();
// 返回 null → 静默(不发 Signal)
if (usage <= 80) return null;
// 返回 { signal, workflow } → 发 Signal,可选触发 Workflow
return {
signal: { cpu: usage, ts: Date.now() },
workflow: null, // 不触发 workflow
};
}
Shell trigger vs Workflow:Sense compute 只能请求 { command: string },由 worker 执行 shell。要跑 workflow,请在命令里调用 CLI(例如 nerve workflow trigger <name> ...)或由外部通过 daemon IPC 触发。
// 示例:异常时在 shell 里触发 workflow(需 PATH 中能调用 nerve)
export async function compute(state) {
const anomaly = detectAnomaly();
if (!anomaly) return { state, trigger: null };
return {
state: { ...state, lastAlert: Date.now() },
trigger: {
command:
'nerve workflow trigger alert --prompt "CPU 持续高负载" --max-rounds 5',
},
};
}
SenseTrigger 类型(@uncaged/nerve-core):{ command: string }。由 parseSenseTrigger 校验(仅允许 command 键)。
trigger |
行为 |
|---|---|
null |
只持久化 state |
{ command } |
持久化 state + worker 执行 shell 命令 |
Drizzle Schema 与迁移
Schema 使用 Drizzle ORM 定义,迁移 SQL 由 drizzle-kit generate 生成:
// senses/cpu-usage/schema.ts
import { sqliteTable, integer, real } from "drizzle-orm/sqlite-core";
export const cpuUsage = sqliteTable("cpu_usage", {
id: integer("id").primaryKey({ autoIncrement: true }),
usage: real("usage").notNull(),
ts: integer("ts").notNull(),
});
生成迁移:
npx drizzle-kit generate # 生成 migrations/0001_*.sql
迁移文件放在 senses/<name>/migrations/,daemon 启动时自动执行。
进程隔离
- 同一
group的 sense 共享一个 long-lived worker 进程 - Worker 通过 Node.js IPC(fork +
process.send)与 kernel 通信 - 每个 sense 有独立的 SQLite 数据库
nerve sense 命令
nerve sense list # 列出所有 sense 及状态(group、throttle、trigger schedule、last signal)
nerve sense trigger <name> # 手动触发一次 compute
nerve sense query <name> # 查询 sense 的 SQLite 数据(默认查 _signals 表)
nerve sense query <name> "SELECT * FROM cpu_usage ORDER BY ts DESC LIMIT 10"
nerve sense schema <name> # 查看 sense 数据库的 CREATE TABLE 语句
nerve sense schema <name> --json # JSON 格式输出
Workflow 开发
Workflow Anatomy
一个 workflow 由以下文件组成(nerve create workflow <name> 自动生成):
workflows/<name>/
index.ts # WorkflowDefinition — import roles,定义 moderator
roles/
planner/
index.ts # role 构建函数
prompt.ts # prompt 模板(纯函数,静态 import)
coder/
index.ts
prompt.ts
tester/
index.ts # 手写 role(纯 CLI 逻辑,不用 factory)
shared.ts # 共享 helpers(provider、工具函数)
types.ts # SenseMeta 类型 + Zod schemas
每个 role 是一个独立目录,包含 index.ts(role 构建函数)和 prompt.ts(prompt 模板)。
设计原则:Meta 只服务 Moderator 路由
Meta 不是数据总线。 Role 之间不通过 meta 传递数据。
content写入 thread,下游 role 通过nerve thread <threadId>自己读上下文meta只包含 moderator 做路由决策需要的最小信息
// ✅ Good — meta 只给 moderator 用
type SenseMeta = {
planner: { senseName: string }; // moderator 不需要 plan 全文
coder: { filesCreated: boolean }; // moderator 只需知道成功没
tester: { passed: boolean; attempt: number };
};
// ❌ Bad — meta 当数据总线,role 间紧耦合
type SenseMeta = {
planner: { plan: string; senseName: string; userInput: string };
coder: { senseName: string; files: Record<string, boolean>; cursorOutput: string };
};
Role Factory 模板(@uncaged/nerve-workflow-utils)
优先使用 Role factory 创建 agent 类 role,不手写 cursorAgent() + llmExtract() 调用链。
// roles/planner/index.ts
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
import { resolveDashScopeProvider, NERVE_ROOT } from "../shared.js";
import { plannerMetaSchema } from "../types.js";
import type { SenseMeta } from "../types.js";
import { plannerPrompt } from "./prompt.js";
export async function buildPlannerRole() {
const provider = await resolveDashScopeProvider();
if (provider === null) {
throw new Error("Missing DASHSCOPE_API_KEY or DASHSCOPE_BASE_URL");
}
return createCursorRole<SenseMeta["planner"]>({
cwd: NERVE_ROOT,
mode: "ask",
prompt: async (threadId) => plannerPrompt({ threadId, ... }),
extract: { provider, schema: plannerMetaSchema },
});
}
可用 factory:
| Factory | 用途 |
|---|---|
createCursorRole(opts) |
调 cursor-agent CLI,适合代码生成/review |
createHermesRole(opts) |
调 Hermes Agent CLI,适合通用任务 |
createLlmRole(opts) |
调 OpenAI 兼容 API,适合纯文本生成 |
createReActRole(opts) |
ReAct 循环(思考→工具→观察),适合需要工具调用的任务 |
所有 factory 内部流程统一:agent 执行 → content → llmExtract(content, schema) → RoleResult<Meta>
不适合用 factory 的场景:纯 CLI 逻辑(如跑测试、检查文件、git 操作)—— 这类 role 手写更清晰。
Prompt 模板
Prompt 放在独立的 prompt.ts 文件,导出纯函数:
// roles/planner/prompt.ts
export function plannerPrompt(vars: {
threadId: string;
senseExamples: string;
nerveYaml: string;
}): string {
return `You are planning a new Nerve sense.
Read the workflow thread: \`nerve thread ${vars.threadId}\`
...`;
}
// roles/planner/index.ts — 使用
import { plannerPrompt } from "./prompt.js";
prompt: async (threadId) => plannerPrompt({ threadId, senseExamples, nerveYaml }),
不要用 readFileSync 读 prompt.md — 静态 import 确保 bundler 能打包,类型安全,无运行时文件依赖。
WorkflowDefinition
// workflows/sense-generator/index.ts
import type { WorkflowDefinition } from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { buildPlannerRole } from "./roles/planner/index.js";
import { buildCoderRole } from "./roles/coder/index.js";
import { tester } from "./roles/tester/index.js";
import type { SenseMeta } from "./roles/types.js";
const workflow: WorkflowDefinition<SenseMeta> = {
name: "sense-generator",
roles: {
planner: await buildPlannerRole(),
coder: await buildCoderRole(),
tester,
},
moderator(context) {
if (context.steps.length === 0) return "planner";
const last = context.steps[context.steps.length - 1];
if (last.role === "planner") return "coder";
if (last.role === "coder") return "tester";
if (last.role === "tester") {
if (last.meta.passed) return END;
return last.meta.attempt < 3 ? "coder" : END;
}
return END;
},
};
export default workflow;
Top-level await 是合法的 — daemon 通过 await import(indexPath) 加载 workflow 模块。
Meta Schema 定义
Meta 类型和 Zod schema 集中在 roles/types.ts:
// roles/types.ts
import { z } from "zod";
export type SenseMeta = {
planner: { senseName: string };
coder: { filesCreated: boolean };
tester: { passed: boolean; attempt: number };
};
export const plannerMetaSchema = z.object({
senseName: z.string().describe("kebab-case sense name from the plan"),
});
export const coderMetaSchema = z.object({
filesCreated: z.boolean().describe("true if the sense files were created"),
});
公共工具(@uncaged/nerve-workflow-utils)
| 工具 | 用途 |
|---|---|
createCursorRole(opts) |
Role factory:cursor-agent + llmExtract,返回 Role<T> |
createHermesRole(opts) |
Role factory:Hermes Agent + llmExtract,返回 Role<T> |
createLlmRole(opts) |
Role factory:OpenAI chat + llmExtract,返回 Role<T> |
createReActRole(opts) |
Role factory:ReAct 循环 + llmExtract,返回 Role<T> |
spawnSafe(cmd, args, opts) |
安全启动子进程,shell: false,支持 timeout,返回 Result<SpawnResult> |
nerveCommandEnv() |
返回配置好 pnpm/nerve PATH 的环境变量 |
isDryRun(startStep) |
从 workflow StartStep 提取 dry-run 标志 |
Workflow 进程模型
- 每个 workflow 类型有一个 on-demand worker 进程(可复用)
- 每个运行实例是一个 Thread,有唯一
runId - 支持崩溃恢复和 resume
nerve workflow & nerve thread 命令
# Workflow 管理
nerve workflow list # 列出 workflow 定义(从 nerve.yaml 读取)
nerve workflow status # 查看运行中的 workflow(从 daemon 实时获取)
nerve workflow trigger <name> # 触发 workflow
nerve workflow trigger <name> --max-rounds 5 --prompt '分析 CPU 异常' --dry-run
# Thread(运行实例)管理
nerve thread list # 列出运行记录(默认只显示 running/queued)
nerve thread list --all # 包括已完成的
nerve thread show <runId> # 查看 role 轮次对话内容
nerve thread inspect <runId> # 查看底层事件详情
nerve thread kill <runId> # 终止运行中的 thread
其他 CLI 命令
初始化与开发
nerve init # 初始化工作区
nerve init --from <git-url> # 从 git 克隆
nerve create sense <name> # 脚手架 sense 模板
nerve create workflow <name> # 脚手架 workflow 模板
nerve dev # 前台开发模式(热重载)
nerve validate # 校验 nerve.yaml
Daemon 管理
nerve daemon start # 后台启动
nerve daemon stop # 停止
nerve daemon status # 状态(pid、uptime、senses、workers)
nerve daemon restart # 重启
nerve status # 同 daemon status
nerve stop # 同 daemon stop
nerve logs # 查看日志
nerve logs -f # 实时跟踪日志
Remote(多节点管理)
nerve remote add <name> <host> --token <secret>
nerve remote list
nerve remote default <name>
nerve --host remote-host:9800 sense list
存储维护
nerve store archive # 归档 30 天前的日志
nerve store archive --vacuum # 归档并压缩 SQLite
编码规范
语言与范式
- Functional-first:用
type+function,不用class+interface - No
this、No inheritance - No optional properties:用
T | null替代?: Result<T, E>类型 处理预期错误,throw仅用于不可恢复的 bug- Always
async/await,不用.then()链 - No dynamic import(除 sense-runtime.ts 和 workflow-worker.ts)
命名
| 类型 | 风格 | 示例 |
|---|---|---|
| 文件 | kebab-case | signal-bus.ts |
| 类型 | PascalCase | SignalBus |
| 函数/变量 | camelCase | createSignalBus |
| 常量 | UPPER_SNAKE | MAX_RETRY_COUNT |
工具链
pnpm run check # biome check(lint + format)
pnpm run format # biome format --write
pnpm run build # 全量构建
pnpm test # 运行测试
Commit 规范
<type>(<scope>): <description>
type: feat | fix | refactor | docs | chore | test
scope: core | cli | daemon | ...
开发流程
- 修改代码 → 在对应 package 目录下开发
pnpm run build→ 确保编译通过pnpm test→ 确保测试通过pnpm run check→ 确保 biome lint/format 通过nerve dev→ 在工作区前台运行验证nerve sense list/nerve sense query <name>→ 检查 sense 输出nerve logs -f→ 实时查看日志排查问题
Pitfalls
- Sense
compute()返回undefined和返回null不同 —undefined会被当作非 null 值尝试持久化 - 同 group 的 sense 共享 worker 进程,一个 sense 的阻塞会影响同组其他 sense
- Workflow 的
moderator必须是纯函数(无副作用),所有副作用放在roles里 - Log 不能触发后续动作 — 这是防止反馈循环的设计
nerve.yaml中所有 duration 字段支持5s、10m等人类可读格式- Workflow directive 格式是
{ workflow: { name, maxRounds, prompt, dryRun } },signal 和 workflow 是蕴含关系(先 signal 后 workflow)