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>
18 KiB
Nerve — AI Agent 观测引擎
Nerve 是一个轻量级观测引擎守护进程。它持续观测外部状态,通过声明式规则响应变化,编排多步骤工作流。
核心架构
External World → Sense(state) → { newState, workflow? } → Workflow → Log
| 概念 | 说明 |
|---|---|
| Sense | 有状态的 compute(state) 函数。返回新状态和可选的 workflow trigger。状态以 JSON 文件持久化。调度由 nerve.yaml 配置。 |
| Workflow | 有状态的多步骤执行。包含 Role(有副作用的执行者)和 Moderator(纯路由器)。每个实例是一个 Thread,有唯一 runId。 |
| Log | 不可变审计日志。记录执行、状态转换、错误。不能触发 Sense(防止反馈循环)。 |
| Engine | 内核,持有 Process Manager、Workflow Manager、Sense Scheduler。不直接加载用户代码。 |
| Daemon | 引擎运行时,作为后台进程运行。 |
关键规则:
- 因果链单向:External → Sense(state) → Workflow(触发时) + Log
- 进程隔离:每个 Sense group 一个 worker(长期),每个 Workflow 类型一个 worker(按需)
- 两个扩展点:Sense(观测什么 + 何时)、Workflow(做什么)
工作区结构
由 nerve init 生成的工作区根目录(默认 ~/.uncaged-nerve/)包含 AGENT.md。实现 sense/workflow 前先阅读该文件。
~/.uncaged-nerve/
├── AGENT.md # 人类 / Agent 可读的工作区约定(init 生成)
├── nerve.yaml # 核心配置
├── package.json # 单一根包(sense/workflow 下不再有独立 package)
├── scripts/build.mjs # 根目录 esbuild;通过 npm/pnpm 的 build 脚本调用
├── senses/
│ └── <name>/
│ └── src/index.ts # exports compute(state) + initialState
├── workflows/
│ └── <name>/
│ ├── index.ts # default export:WorkflowDefinition
│ ├── moderator.ts # 可选:抽出 moderator,由 index 导入
│ ├── build.ts # 可选:共享常量 / 纯函数(避免 index 臃肿;非 esbuild 入口)
│ └── roles/
│ └── <role>.ts # 每角色单文件(推荐平铺,而非 roles/<role>/index.ts)
└── data/
├── senses/ # sense 状态 JSON 文件(自动生成)
└── logs.db # 日志存储(自动生成)
命名约定
- Workflow:动词开头的 kebab-case(例如
review-pull-request、deploy-staging)。避免单独名词式命名(如notifications)。 - Sense:描述性名词 kebab-case(例如
cpu-usage)。
CLI 完整参考
全局选项:--host <host:port>(连接远程 daemon)、--api-token <secret>(Bearer 认证)
初始化与脚手架
nerve init # 初始化工作区
nerve init --from <git-url> # 从 git 仓库克隆工作区
nerve init workspace # 只初始化工作区结构
nerve create sense <name> # 创建 sense 脚手架
nerve create sense <name> --force # 覆盖已有
nerve create workflow <name> # 创建 workflow 脚手架
nerve create workflow <name> --force
nerve validate # 验证 nerve.yaml 配置
Daemon 管理
nerve daemon start # 启动后台 daemon
nerve daemon start --port 3000 # 指定 HTTP API 端口
nerve daemon stop # 停止 daemon
nerve daemon restart # 重启
nerve daemon status # 查看状态
nerve daemon logs # 查看日志
nerve daemon logs --follow # 实时日志
nerve daemon logs --n 50 # 最近 50 行
nerve dev # 前台开发模式(不 fork daemon)
nerve dev --port 3000 # 指定端口
Sense 操作
nerve sense list # 列出所有注册的 sense
nerve sense trigger <name> # 手动触发 sense 计算
Workflow 操作
nerve workflow list # 列出 nerve.yaml 中定义的 workflow
nerve workflow status # 查看运行中的 workflow 状态
nerve workflow trigger <name> # 触发 workflow
nerve workflow trigger <name> --prompt "检查生产环境"
nerve workflow trigger <name> --maxRounds 50
nerve workflow trigger <name> --dryRun # 干跑模式
Thread(Workflow 执行记录)
nerve thread list # 列出最近的 workflow 执行
nerve thread list --all # 包含已完成/失败的
nerve thread list --workflow <name> # 按 workflow 过滤
nerve thread list --limit 50 # 最多 50 条
nerve thread show <runId> # 查看 role 对话轮次
nerve thread show <runId> --budget 16000 # 增大输出预算(默认 8000 字符)
nerve thread inspect <runId> # 查看详情和事件
nerve thread kill <runId> # 终止运行中/排队中的 thread
Store(日志归档)
nerve store archive # 导出旧日志到 JSONL 归档
nerve store archive --vacuum # 归档后 VACUUM 数据库
Knowledge(知识库)
nerve knowledge sync # 从 knowledge.yaml 重建索引
nerve knowledge query "搜索内容" # 搜索知识库
nerve knowledge query "内容" --limit 5
nerve knowledge query "内容" -g # 搜索所有注册仓库
Remote(远程 daemon)
nerve remote add <name> <host:port> --token <secret>
nerve remote list
nerve remote show <name>
nerve remote set-url <name> <host>
nerve remote set-token <name> <token>
nerve remote remove <name>
nerve remote default <name> # 设为默认远程
Agent(向 AI Agent 注入 nerve skill)
nerve agent status # CLI 版本与各注入目录中的 skill 版本
nerve agent inject hermes # 安装到 ~/.hermes/skills/nerve
nerve agent inject hermes --profile <name> # 写入 ~/.hermes/profiles/<name>/skills/nerve
nerve agent inject cursor # 注入到当前项目 .cursorrules
nerve agent inject claude # 注入到 ~/.claude/CLAUDE.md
nerve agent update # 将所有已注入目录更新到当前 CLI 对应版本
nerve agent remove hermes # 移除 Hermes 注入
nerve agent remove cursor # 移除 Cursor 注入
nerve agent remove claude # 移除 Claude Code 注入
nerve.yaml 配置参考
# 引擎全局配置
max_rounds: 100 # moderator 最大轮次(默认 100)
# Sense 配置
senses:
cpu-usage:
group: system # 必填,同 group 的 sense 共享 worker
interval: 10s # 轮询间隔(duration: 5s, 10m, 1h)
throttle: 5s # 最小计算间隔
timeout: 10s # compute 超时
grace_period: null # 超时后优雅等待
system-health:
group: derived
on: [cpu-usage, disk-usage] # 响应式:被列出的 sense 完成 compute 时触发
throttle: null
timeout: null
# Workflow 配置
workflows:
my-workflow:
concurrency: 1 # 必填,并发数
overflow: drop # 必填,超并发时处理:drop | queue
max_queue: 100 # overflow=queue 时的队列上限(默认 100)
# HTTP API
api:
port: 3000 # null = 不启用 HTTP
host: "127.0.0.1" # 监听地址
token: null # 非 loopback 时必填
# LLM Extract(可选)
extract:
provider: anthropic
model: claude-sonnet-4-20250514
Sense 开发指南
compute 函数签名
Sense 的 compute 接收当前状态,返回新状态和可选的 shell trigger({ command: string })。状态以 JSON 文件持久化在 data/senses/<name>.json。Workflow 只能通过 CLI / daemon IPC 启动,不能从 sense 返回值直接启动。
import type { SenseComputeFn } from "@uncaged/nerve-core";
type MyState = {
lastRun: number | null;
count: number;
};
export const initialState: MyState = { lastRun: null, count: 0 };
export async function compute(state: MyState): Promise<{
state: MyState;
trigger: { command: string } | null;
}> {
return {
state: { lastRun: Date.now(), count: state.count + 1 },
trigger: null,
};
}
状态持久化
| 时机 | 行为 |
|---|---|
| Worker 启动 | 读取 data/senses/<name>.json;不存在或损坏则使用 initialState |
| Compute 成功 | 原子写入新状态(temp + rename),然后更新内存 |
| Compute 失败 | 状态不变(磁盘和内存都不变) |
| Daemon 重启 | 从上次成功写入恢复 |
返回值
// trigger: null → 不执行 shell 命令
// trigger: { command } → sense worker 在成功的 compute 后以 shell 执行该命令(cwd = nerve 根目录)
// 启动 workflow:在 shell 中调用 `nerve workflow trigger ...`,或使用 daemon IPC / HTTP API
Sense 模块导出
每个 sense 的 src/index.ts 必须导出两个东西:
// senses/<name>/src/index.ts
// 1. 初始状态
export const initialState: MyState = { ... };
// 2. compute 函数
export async function compute(state: MyState): Promise<{
state: MyState;
trigger: { command: string } | null;
}> {
// ...
}
调度方式
- interval 轮询:
interval: 10s— 每 10 秒执行一次 - 响应式触发:
on: [cpu-usage]— 当 cpu-usage 完成 compute 后触发 - 两者可以组合
调试
nerve dev # 前台运行,看实时输出
nerve sense trigger <name> # 手动触发一次
完整示例:CPU 监控
// senses/cpu-usage/src/index.ts
import { loadavg } from "node:os";
type CpuState = {
samples: Array<{ ts: number; value: number }>;
};
export const initialState: CpuState = { samples: [] };
export async function compute(state: CpuState): Promise<{
state: CpuState;
trigger: null;
}> {
const [oneMin] = loadavg();
const value = typeof oneMin === "number" && !Number.isNaN(oneMin) ? oneMin : 0;
const newSamples = [...state.samples.slice(-99), { ts: Date.now(), value }];
return { state: { samples: newSamples }, trigger: null };
}
nerve.yaml:
senses:
cpu-usage:
group: system
interval: 10s
throttle: 5s
timeout: 10s
Workflow 开发指南
核心类型
import type {
WorkflowDefinition,
RoleResult,
ThreadContext,
RoleMeta,
Moderator,
} from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
// Role<Meta> — (ctx: ThreadContext) => Promise<RoleResult<Meta>>
// RoleResult<Meta> — { content: string; meta: Meta }
// ThreadContext<M extends RoleMeta> — threadId, start(__start__ 帧), steps(各 role 轮次)
// Moderator<M> — (ctx) => 下一个 role 名 | END
// WorkflowDefinition<M extends RoleMeta> — name, roles, moderator
createRole 四元组(接入 LLM 时推荐)
工作区根目录需安装 @uncaged/nerve-workflow-utils(及所选 agent 适配器包)。默认 nerve init 的 package.json 不含该依赖时,在 ~/.uncaged-nerve 下执行 pnpm add @uncaged/nerve-workflow-utils(或 npm 等价命令)。
使用 createRole,按固定顺序传入四件事:
- adapter —
AgentFn,(ctx, systemPrompt) => Promise<string>(原始模型输出文本)。 - prompt —
string,或async (ctx: ThreadContext) => string。 - meta —
z.ZodType<M>,供 moderator 路由的结构化 meta。 - extract —
{ provider: LlmProvider; dryRun: boolean | null },声明从回复中抽取 meta 时用的 LLM(OpenAI 兼容)及是否 dry-run。
import { createLlmAdapter, createRole } from "@uncaged/nerve-workflow-utils";
import type { ThreadContext } from "@uncaged/nerve-core";
import { z } from "zod";
const provider = {
baseUrl: "https://api.example.com/v1",
apiKey: process.env.EXAMPLE_API_KEY!,
model: "gpt-4o-mini",
};
const planMeta = z.object({ next: z.enum(["execute", "stop"]) });
export const planner = createRole(
createLlmAdapter(provider),
async (ctx: ThreadContext) => `规划任务:${ctx.start.content}`,
planMeta,
{ provider, dryRun: null },
);
createLlmAdapter 仅位于 @uncaged/nerve-workflow-utils:用 LlmProvider 生成 AgentFn,单轮对话里 system 来自 createRole 解析后的 prompt 字符串,user 为线程起点 ctx.start.content。
基本 Workflow 示例(平铺 roles/<role>.ts)
// workflows/example/roles/main.ts
import type { RoleResult, ThreadContext } from "@uncaged/nerve-core";
export async function main(ctx: ThreadContext): Promise<RoleResult<{ round: number }>> {
const prompt = ctx.start.content;
return {
content: `处理完成: ${prompt}`,
meta: { round: ctx.steps.length },
};
}
// workflows/example/index.ts
import type { ThreadContext, WorkflowDefinition } from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { main } from "./roles/main.js";
type Meta = Record<"main", { round: number }>;
const workflow: WorkflowDefinition<Meta> = {
name: "example",
roles: { main },
moderator(ctx: ThreadContext<Meta>) {
return ctx.steps.length === 0 ? "main" : END;
},
};
export default workflow;
可选:将 moderator 挪到 moderator.ts 再 import { route } from "./moderator.js",保持 index.ts 只负责组装 WorkflowDefinition。
多 Role Workflow 示例
// workflows/plan-execute-review/roles/planner.ts
import type { RoleResult, ThreadContext } from "@uncaged/nerve-core";
export async function planner(ctx: ThreadContext): Promise<RoleResult<{ status: string }>> {
void ctx;
return { content: "计划: ...", meta: { status: "planned" } };
}
// workflows/plan-execute-review/roles/executor.ts
import type { RoleResult, ThreadContext } from "@uncaged/nerve-core";
export async function executor(ctx: ThreadContext): Promise<RoleResult<{ status: string }>> {
void ctx;
return { content: "执行: ...", meta: { status: "executed" } };
}
// workflows/plan-execute-review/roles/reviewer.ts
import type { RoleResult, ThreadContext } from "@uncaged/nerve-core";
export async function reviewer(ctx: ThreadContext): Promise<RoleResult<{ status: string }>> {
void ctx;
return { content: "审核通过", meta: { status: "approved" } };
}
// workflows/plan-execute-review/index.ts
import type { WorkflowDefinition, ThreadContext } from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { executor } from "./roles/executor.js";
import { planner } from "./roles/planner.js";
import { reviewer } from "./roles/reviewer.js";
type Roles = Record<"planner" | "executor" | "reviewer", { status: string }>;
const workflow: WorkflowDefinition<Roles> = {
name: "plan-execute-review",
roles: { planner, executor, reviewer },
moderator(ctx: ThreadContext<Roles>) {
if (ctx.steps.length === 0) return "planner";
const last = ctx.steps[ctx.steps.length - 1];
if (last.role === "planner") return "executor";
if (last.role === "executor") return "reviewer";
return END;
},
};
export default workflow;
Agent 适配器
Workflow role 可以集成 AI agent。已知适配器 ID:echo、cursor、hermes、codex。
type AgentFn = (ctx: ThreadContext, systemPrompt: string) => Promise<string>;
没有现成 agent 包时,用 createLlmAdapter(@uncaged/nerve-workflow-utils) 从 OpenAI 兼容的 LlmProvider 构造 AgentFn,再交给 createRole 的四元组。
Workflow 运行状态
queued → started → completed | failed | crashed | killed | interrupted | dropped
日常操作 Pattern
查看系统整体状态
nerve daemon status # daemon 是否在运行
nerve sense list # 所有 sense 及其调度配置
nerve workflow status # 运行中的 workflow
nerve thread list # 最近的 workflow 执行记录
手动触发 workflow
nerve workflow trigger my-workflow --prompt "手动检查"
nerve thread list --workflow my-workflow # 查看执行状态
nerve thread show <runId> # 查看对话详情
排查 sense 报错
nerve daemon logs --follow # 查看实时日志
nerve sense trigger <name> # 手动触发看报错
nerve dev # 前台模式,更详细的输出
开发新 sense
nerve create sense my-sensor # 脚手架
# 编辑 senses/my-sensor/src/index.ts — 实现 compute(state) + initialState
nerve validate # 验证配置
nerve dev # 前台测试
nerve sense trigger my-sensor # 单次触发验证
开发新 workflow
nerve create workflow my-flow # 脚手架
# 推荐对齐 AGENT.md:workflows/my-flow/index.ts + roles/<role>.ts(平铺),moderator 可拆到 moderator.ts
nerve validate # 验证配置
cd ~/.uncaged-nerve && npm run build # 工作区根目录构建;勿在单个 workflow 子目录单独跑 build
nerve workflow trigger my-flow --prompt "测试" --dryRun # 干跑
nerve thread show <runId> # 查看执行轨迹
Pitfalls
- Sense 状态:
compute(state)必须返回{ state, workflow }对象。不要返回 null 或 undefined。 - initialState:每个 sense 必须导出
initialState,否则加载失败。 - 状态持久化:daemon 自动管理状态读写,业务代码不要自行读写 state.json。
- no optional properties:nerve 代码规范禁止
?:,用T | null代替。 - 函数式风格:用
function+type,不用class+interface。 - workflow 用 default export:工作区里通常只有
workflows/<name>/index.ts使用 default export(daemon 加载约定)。 - concurrency + overflow:workflow 必须配置并发策略,否则验证失败。
- moderator 是同步函数:不要加 async,moderator 是纯路由逻辑,不能有副作用。