This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
nerve/packages/cli/skills/claude/CLAUDE.md
T
xiaoju 8dd82d99da refactor(core): remove WorkflowTrigger from SenseTrigger — shell only
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>
2026-05-02 12:33:38 +00:00

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-requestdeploy-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;
}> {
  // ...
}

调度方式

  1. interval 轮询interval: 10s — 每 10 秒执行一次
  2. 响应式触发on: [cpu-usage] — 当 cpu-usage 完成 compute 后触发
  3. 两者可以组合

调试

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 initpackage.json 不含该依赖时,在 ~/.uncaged-nerve 下执行 pnpm add @uncaged/nerve-workflow-utils(或 npm 等价命令)。

使用 createRole,按固定顺序传入四件事:

  1. adapterAgentFn(ctx, systemPrompt) => Promise<string>(原始模型输出文本)。
  2. promptstring,或 async (ctx: ThreadContext) => string
  3. metaz.ZodType<M>,供 moderator 路由的结构化 meta。
  4. 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.tsimport { 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。已知适配器 IDechocursorhermescodex

type AgentFn = (ctx: ThreadContext, systemPrompt: string) => Promise<string>;

没有现成 agent 包时,用 createLlmAdapter@uncaged/nerve-workflow-utils 从 OpenAI 兼容的 LlmProvider 构造 AgentFn,再交给 createRole 的四元组。

Workflow 运行状态

queuedstartedcompleted | 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 是纯路由逻辑,不能有副作用。