docs(nerve-dev): update workflow section with role factories, meta routing principle, prompt.ts pattern
- Role factory templates (createCursorRole, createHermesRole, createLlmRole, createReActRole)
- Meta = moderator routing only, not data bus between roles
- prompt.ts pure functions instead of readFileSync + prompt.md
- Updated workflow-utils API table
- Real sense-generator example throughout
小橘 🍊(NEKO Team)
This commit is contained in:
@@ -215,79 +215,177 @@ nerve sense schema <name> --json # JSON 格式输出
|
||||
workflows/<name>/
|
||||
index.ts # WorkflowDefinition — import roles,定义 moderator
|
||||
roles/
|
||||
analyst/
|
||||
index.ts # role execute 函数
|
||||
prompt.md # prompt 模板(可选)
|
||||
reporter/
|
||||
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`(execute 函数)和可选的 prompt 模板、配置等资源文件。
|
||||
每个 role 是一个独立目录,包含 `index.ts`(role 构建函数)和 `prompt.ts`(prompt 模板)。
|
||||
|
||||
### 设计原则:Meta 只服务 Moderator 路由
|
||||
|
||||
**Meta 不是数据总线。** Role 之间不通过 meta 传递数据。
|
||||
|
||||
- `content` 写入 thread,下游 role 通过 `nerve thread <threadId>` 自己读上下文
|
||||
- `meta` 只包含 moderator 做路由决策需要的最小信息
|
||||
|
||||
```typescript
|
||||
// ✅ 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()` 调用链。
|
||||
|
||||
```typescript
|
||||
// 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` 文件,导出纯函数:
|
||||
|
||||
```typescript
|
||||
// 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}\`
|
||||
...`;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// roles/planner/index.ts — 使用
|
||||
import { plannerPrompt } from "./prompt.js";
|
||||
|
||||
prompt: async (threadId) => plannerPrompt({ threadId, senseExamples, nerveYaml }),
|
||||
```
|
||||
|
||||
**不要用 `readFileSync` 读 prompt.md** — 静态 import 确保 bundler 能打包,类型安全,无运行时文件依赖。
|
||||
|
||||
### WorkflowDefinition
|
||||
|
||||
```typescript
|
||||
// workflows/alert/index.ts
|
||||
// workflows/sense-generator/index.ts
|
||||
import type { WorkflowDefinition } from "@uncaged/nerve-core";
|
||||
import { analyst } from "./roles/analyst/index.js";
|
||||
import { reporter } from "./roles/reporter/index.js";
|
||||
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";
|
||||
|
||||
export const definition: WorkflowDefinition = {
|
||||
name: "alert",
|
||||
roles: { analyst, reporter },
|
||||
|
||||
// Moderator: 纯路由函数,决定下一个 role
|
||||
moderator: (context) => {
|
||||
if (context.rounds === 0) return "analyst";
|
||||
if (context.rounds === 1) return "reporter";
|
||||
return "END";
|
||||
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;
|
||||
```
|
||||
|
||||
```typescript
|
||||
// workflows/alert/roles/analyst/index.ts
|
||||
export const analyst = {
|
||||
async execute(start, messages) {
|
||||
// start: { prompt, maxRounds, dryRun }
|
||||
// messages: 历史消息数组
|
||||
const analysis = await callLLM(start.prompt);
|
||||
return { content: analysis, meta: {} };
|
||||
},
|
||||
};
|
||||
```
|
||||
Top-level `await` 是合法的 — daemon 通过 `await import(indexPath)` 加载 workflow 模块。
|
||||
|
||||
Role 可以附带 prompt 模板:
|
||||
### Meta Schema 定义
|
||||
|
||||
Meta 类型和 Zod schema 集中在 `roles/types.ts`:
|
||||
|
||||
```typescript
|
||||
// workflows/alert/roles/reporter/index.ts
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
// roles/types.ts
|
||||
import { z } from "zod";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROMPT = readFileSync(join(__dirname, "prompt.md"), "utf8");
|
||||
|
||||
export const reporter = {
|
||||
async execute(start, messages) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
await sendNotification(PROMPT.replace("{{content}}", lastMessage.content));
|
||||
return { content: "已通知", meta: {} };
|
||||
},
|
||||
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`)
|
||||
|
||||
Role 中可使用以下公共工具:
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `spawnSafe(cmd, args, opts)` | 安全启动子进程,`shell: false`,支持 timeout 和 dry-run,返回 `Result<SpawnResult>` |
|
||||
| `nerveCommandEnv()` | 返回配置好 pnpm/nerve PATH 的环境变量,用于在 role 中调用 CLI |
|
||||
| `cursorAgent(prompt, opts)` | 调用 cursor-agent CLI(mode: `"plan"` / `"ask"` / `"default"`),返回 `Result<string>` |
|
||||
| `llmExtract(opts)` | 调用 OpenAI 兼容 API + Zod schema 提取结构化数据,返回 `Result<T>` |
|
||||
| `readNerveYaml(dir)` | 安全读取 nerve.yaml(防路径穿越),返回 `Result<string>` |
|
||||
| `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 进程模型
|
||||
|
||||
Reference in New Issue
Block a user