diff --git a/packages/cli/skills/hermes/SKILL.md b/packages/cli/skills/hermes/SKILL.md index 69cab6d..0377c1f 100644 --- a/packages/cli/skills/hermes/SKILL.md +++ b/packages/cli/skills/hermes/SKILL.md @@ -36,23 +36,34 @@ External World → Sense → Signal → Workflow → Log ## 工作区结构 +由 `nerve init` 生成的工作区根目录(默认 `~/.uncaged-nerve/`)包含 **`AGENT.md`**。实现 sense/workflow 前先阅读该文件:它与本文 skill 对齐,约定目录布局、`createRole` 用法以及**始终在仓库根目录**执行的构建命令。 + ``` -~/.uncaged-nerve/ # 默认工作区(nerve init 创建) +~/.uncaged-nerve/ +├── AGENT.md # 人类 / Agent 可读的工作区约定(init 生成) ├── nerve.yaml # 核心配置 +├── package.json # 单一根包(sense/workflow 下不再有独立 package) +├── scripts/build.mjs # 根目录 esbuild;通过 npm/pnpm 的 build 脚本调用 ├── senses/ │ └── / │ ├── src/index.ts # exports compute() + table -│ ├── src/schema.ts # drizzle 表定义 +│ ├── src/schema.ts # Drizzle 表定义 │ └── migrations/ # SQL 迁移 ├── workflows/ │ └── / -│ ├── index.ts # exports WorkflowDefinition -│ └── roles// -│ ├── index.ts # role 实现 -│ └── prompt.md # 可选 system prompt +│ ├── index.ts # default export:WorkflowDefinition +│ ├── moderator.ts # 可选:抽出 moderator,由 index 导入 +│ ├── build.ts # 可选:共享常量 / 纯函数(避免 index 臃肿;非 esbuild 入口) +│ └── roles/ +│ └── .ts # 每角色单文件(推荐平铺,而非 roles//index.ts) └── data/ # 运行时数据(SQLite、blobs) ``` +### 命名约定 + +- **Workflow**:动词开头的 kebab-case(例如 `review-pull-request`、`deploy-staging`)。避免单独名词式命名(如 `notifications`)。 +- **Sense**:描述性名词 kebab-case(例如 `cpu-usage`)。 + --- ## CLI 完整参考 @@ -156,6 +167,17 @@ nerve remote remove nerve remote default # 设为默认远程 ``` +### Agent(向 Hermes 注入本 skill) + +```bash +nerve agent status # CLI 版本与各 Hermes 注入目录中的 skill 版本 +nerve agent inject hermes # 安装到 ~/.hermes/skills/nerve +nerve agent inject hermes --profile # 写入 ~/.hermes/profiles//skills/nerve +nerve agent update # 将所有已注入目录更新到当前 CLI 对应版本 +nerve agent remove hermes # 移除默认 profile 的注入 +nerve agent remove hermes --profile +``` + --- ## nerve.yaml 配置参考 @@ -205,22 +227,27 @@ extract: ### compute 函数签名 -```typescript -import type { LibSQLDatabase } from "drizzle-orm/libsql"; -import type { ComputeResult, WorkflowTrigger } from "@uncaged/nerve-core"; +Sense 的 `compute` **无参数**。它不接收数据库句柄:daemon 在 worker 内调用 `SenseComputeFn`,由运行时负责把非 null 结果的 `signal` 写入该 sense 的 Drizzle 表并记入 `_signals`。超时由运行时控制(对应 `nerve.yaml` 里的 `timeout`),无需在业务代码里读取 `AbortSignal`。 -export async function compute( - db: LibSQLDatabase, // 此 sense 的 Drizzle ORM 数据库 - peers: Record, // 其他 sense 的数据库(只读) - options: { signal: AbortSignal }, // 超时 abort signal -): Promise> +```typescript +import type { ComputeResult, SenseComputeFn } from "@uncaged/nerve-core"; + +export const compute: SenseComputeFn = async () => { + // ... +}; +// 或等价地: +export async function compute(): Promise> { + // ... +} ``` +(运行时定义见 `@uncaged/nerve-core` 的 `SenseComputeFn` / `SenseModule`,daemon 侧在 `sense-runtime.ts` 的 `executeCompute` 中插入 `result.signal`。) + ### 返回值 ```typescript // 返回 null = 静默,不发 signal -// 返回非 null = 发出 signal,可选触发 workflow +// 返回非 null = 发出 signal(并写入业务表),可选触发 workflow type ComputeResult = | null | { signal: T; workflow: WorkflowTrigger | null }; @@ -233,21 +260,20 @@ type WorkflowTrigger = { }; ``` +若返回值是普通对象且不含 `signal` 字段,内核会按 shorthand 视为 `{ signal: payload, workflow: null }`(见 core 的 `routeSenseComputeOutput`)。 + ### Sense 模块导出 ```typescript // senses//src/index.ts -import type { SenseModule, ComputeResult } from "@uncaged/nerve-core"; +import type { ComputeResult } from "@uncaged/nerve-core"; import { table } from "./schema.js"; -export async function compute( - db: LibSQLDatabase, - _peers: Record, - _options: { signal: AbortSignal }, -): Promise> { - const value = Math.random(); // 替换为真实观测逻辑 - await db.insert(table).values({ ts: Date.now(), value }); - return { signal: value, workflow: null }; +type Row = { ts: number; value: number }; + +export async function compute(): Promise> { + const row: Row = { ts: Date.now(), value: Math.random() }; // 替换为真实观测逻辑 + return { signal: row, workflow: null }; } export { table }; @@ -292,18 +318,14 @@ export const table = sqliteTable("samples", { // senses/cpu-usage/src/index.ts import os from "node:os"; -import type { LibSQLDatabase } from "drizzle-orm/libsql"; import type { ComputeResult } from "@uncaged/nerve-core"; import { table } from "./schema.js"; -export async function compute( - db: LibSQLDatabase, - _peers: Record, - _options: { signal: AbortSignal }, -): Promise> { +type Row = { ts: number; value: number }; + +export async function compute(): Promise> { const oneMin = os.loadavg()[0]; - await db.insert(table).values({ ts: Date.now(), value: oneMin }); - return { signal: oneMin, workflow: null }; + return { signal: { ts: Date.now(), value: oneMin }, workflow: null }; } export { table }; @@ -331,55 +353,80 @@ import type { WorkflowDefinition, RoleResult, ThreadContext, - StartStep, - RoleStep, + RoleMeta, + Moderator, } from "@uncaged/nerve-core"; import { END } from "@uncaged/nerve-core"; -// Role:执行者,接收上下文返回结果 -type Role = (ctx: ThreadContext) => Promise>; -type RoleResult = { content: string; meta: Meta }; - -// Moderator:路由器,决定下一个 role 或结束 -type Moderator = (ctx: ThreadContext) => (keyof M & string) | typeof END; - -// ThreadContext:对话上下文 -type ThreadContext = { - threadId: string; - start: StartStep; // 初始 prompt(role: "__start__") - steps: RoleStep[]; // 所有 role 的执行记录 -}; - -// WorkflowDefinition:完整定义 -type WorkflowDefinition = { - name: string; - roles: { [K in keyof M & string]: Role }; - moderator: Moderator; -}; +// Role — (ctx: ThreadContext) => Promise> +// RoleResult — { content: string; meta: Meta } +// ThreadContext — threadId, start(__start__ 帧), steps(各 role 轮次) +// Moderator — (ctx) => 下一个 role 名 | END +// WorkflowDefinition — name, roles, moderator ``` -### 基本 Workflow 示例 +### createRole 四元组(接入 LLM 时推荐) + +工作区根目录需安装 **`@uncaged/nerve-workflow-utils`**(及所选 agent 适配器包)。默认 `nerve init` 的 `package.json` 不含该依赖时,在 `~/.uncaged-nerve` 下执行 `pnpm add @uncaged/nerve-workflow-utils`(或 npm 等价命令)。 + +使用 **`createRole`**,按固定顺序传入四件事: + +1. **adapter** — `AgentFn`,`(ctx, systemPrompt) => Promise`(原始模型输出文本)。 +2. **prompt** — `string`,或 `async (ctx: ThreadContext) => string`。 +3. **meta** — `z.ZodType`,供 moderator 路由的结构化 meta。 +4. **extract** — `{ provider: LlmProvider; dryRun: boolean | null }`,声明从回复中抽取 meta 时用的 LLM(OpenAI 兼容)及是否 dry-run。 ```typescript -// workflows/example/index.ts -import type { RoleResult, ThreadContext, WorkflowDefinition } from "@uncaged/nerve-core"; -import { END } from "@uncaged/nerve-core"; +import { createLlmAdapter, createRole } from "@uncaged/nerve-workflow-utils"; +import type { ThreadContext } from "@uncaged/nerve-core"; +import { z } from "zod"; -type Meta = Record<"main", { round: number }>; +const provider = { + baseUrl: "https://api.example.com/v1", + apiKey: process.env.EXAMPLE_API_KEY!, + model: "gpt-4o-mini", +}; -async function main(ctx: ThreadContext): Promise> { +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/.ts`) + +```typescript +// workflows/example/roles/main.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + +export async function main(ctx: ThreadContext): Promise> { const prompt = ctx.start.content; return { content: `处理完成: ${prompt}`, meta: { round: ctx.steps.length }, }; } +``` + +```typescript +// 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 = { name: "example", roles: { main }, moderator(ctx: ThreadContext) { - // 执行一次 main 就结束 return ctx.steps.length === 0 ? "main" : END; }, }; @@ -387,25 +434,50 @@ const workflow: WorkflowDefinition = { export default workflow; ``` +可选:将 `moderator` 挪到 `moderator.ts` 再 `import { route } from "./moderator.js"`,保持 `index.ts` 只负责组装 `WorkflowDefinition`。 + ### 多 Role Workflow 示例 ```typescript -import type { WorkflowDefinition, RoleResult, ThreadContext } from "@uncaged/nerve-core"; -import { END } from "@uncaged/nerve-core"; +// workflows/plan-execute-review/roles/planner.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; -type Roles = Record<"planner" | "executor" | "reviewer", { status: string }>; - -async function planner(ctx: ThreadContext): Promise> { +export async function planner(ctx: ThreadContext): Promise> { + void ctx; return { content: "计划: ...", meta: { status: "planned" } }; } +``` -async function executor(ctx: ThreadContext): Promise> { +```typescript +// workflows/plan-execute-review/roles/executor.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + +export async function executor(ctx: ThreadContext): Promise> { + void ctx; return { content: "执行: ...", meta: { status: "executed" } }; } +``` -async function reviewer(ctx: ThreadContext): Promise> { +```typescript +// workflows/plan-execute-review/roles/reviewer.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + +export async function reviewer(ctx: ThreadContext): Promise> { + void ctx; return { content: "审核通过", meta: { status: "approved" } }; } +``` + +```typescript +// 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 = { name: "plan-execute-review", @@ -424,12 +496,14 @@ export default workflow; ### Agent 适配器 -Workflow role 可以集成 AI agent。已知适配器 ID:`echo`、`cursor`、`hermes`、`codex`。 +Workflow role 可以集成 AI agent。已知适配器 **ID**:`echo`、`cursor`、`hermes`、`codex`。 ```typescript type AgentFn = (ctx: ThreadContext, systemPrompt: string) => Promise; ``` +没有现成 agent 包时,用 **`createLlmAdapter`(`@uncaged/nerve-workflow-utils`)** 从 OpenAI 兼容的 `LlmProvider` 构造 `AgentFn`,再交给 **`createRole`** 的四元组。 + ### Workflow 运行状态 `queued` → `started` → `completed` | `failed` | `crashed` | `killed` | `interrupted` | `dropped` @@ -484,9 +558,10 @@ nerve sense query my-sensor "SELECT * FROM ..." # 检查数据 ### 开发新 workflow ```bash -nerve create workflow my-flow # 脚手架 -# 编辑 workflows/my-flow/index.ts 和 roles/ +nerve create workflow my-flow # 脚手架(当前 CLI 可能仍生成 roles// 子目录) +# 推荐对齐 AGENT.md:workflows/my-flow/index.ts + roles/.ts(平铺),moderator 可拆到 moderator.ts nerve validate # 验证配置 +cd ~/.uncaged-nerve && npm run build # 工作区根目录构建(等价:pnpm run build);勿在单个 workflow 子目录单独跑 build nerve workflow trigger my-flow --prompt "测试" --dryRun # 干跑 nerve thread show # 查看执行轨迹 ``` @@ -496,10 +571,10 @@ nerve thread show # 查看执行轨迹 ## Pitfalls - **Sense 返回值**:返回 `null` 表示静默(不发 signal);返回 `{ signal, workflow }` 才发 signal。不要返回 undefined。 +- **Sense 持久化**:daemon 在 `compute()` 返回非 null 时自动执行 `db.insert(table).values(signal)` 并写入 `_signals`;业务代码不要自行 insert。 - **no optional properties**:nerve 代码规范禁止 `?:`,用 `T | null` 代替。 - **函数式风格**:用 `function` + `type`,不用 `class` + `interface`。 -- **workflow 用 default export**:这是唯一允许 default export 的场景。 +- **workflow 用 default export**:工作区里通常只有 `workflows//index.ts` 使用 default export(daemon 加载约定)。 - **_signals 表**:每个 sense 自动有 `_signals` 表记录 signal 历史,受 `retention` 配置限制。 -- **peers 只读**:sense 的 `peers` 参数提供其他 sense 数据库的只读访问,不要写入。 - **concurrency + overflow**:workflow 必须配置并发策略,否则验证失败。 - **moderator 是同步函数**:不要加 async,moderator 是纯路由逻辑,不能有副作用。