Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49f3d91d1b | |||
| 4d75c8683f | |||
| 99b0e58fb6 | |||
| 1849789c02 |
@@ -0,0 +1,146 @@
|
||||
# Nerve 单仓死代码分析报告
|
||||
|
||||
**分析日期**: 2026-04-30
|
||||
**范围**: `packages/core`, `daemon`, `store`, `cli`, `khala`, `workflow-utils`, `workflow-meta`, `adapter-cursor`, `adapter-hermes` 的 TypeScript 源码与 `package.json` 依赖。
|
||||
**方法**: 全仓 `ripgrep` 交叉验证 `import` / `export` 路径;未运行 Knip/TS 死代码专项工具。
|
||||
**说明**: 未包含 `role-reviewer` / `role-committer` / `skills` 等包(你列出的范围外),但 `workflow-meta` 对其中部分有依赖,相关结论已注明。
|
||||
|
||||
---
|
||||
|
||||
## 方法限制(读前必读)
|
||||
|
||||
| 限制 | 影响 |
|
||||
|------|------|
|
||||
| **动态 `import(url)`** | `cli` 的 `loadDaemonModule` 在运行时加载 `@uncaged/nerve-daemon`,静态分析无法把 `createKernel` 等映射到具体导出。 |
|
||||
| **已发布包的公共 API** | 大量导出仅被仓外消费者使用;本报告中的「仓内未引用」≠「应删除」。 |
|
||||
| **构建入口** | Rslib 多入口(如 `sense-worker`、`workflow-worker`、`daemon-bootstrap`)视为存活入口,不作为孤儿文件。 |
|
||||
| **内部未导出函数** | 未做逐函数调用图;「死函数」仅列出高置信个例。 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 未使用导出(仓内无 `import`)
|
||||
|
||||
以下符号在 **monorepo 内** 没有任何文件从 `@uncaged/nerve-core` / `@uncaged/nerve-workflow-utils` / `@uncaged/nerve-daemon` 等包根导出处引用(测试与定义自身除外)。
|
||||
|
||||
### 1.1 `@uncaged/nerve-core`
|
||||
|
||||
| 路径 | 未使用项 | 置信度 | 建议 |
|
||||
|------|----------|--------|------|
|
||||
| `packages/core/src/index.ts` → `agent.js` | `KNOWN_AGENT_ADAPTER_IDS` | **高** | **调查**:若 CLI/校验应约束 adapter id,可接入;否则可从公共 API 移除或保留作文档性常量并注明。 |
|
||||
| `packages/core/src/index.ts` → `sense.js` | `labelSenseTrigger`(`senseTriggerLabels` 在 `cli`/`daemon` 有使用) | **高(相对仓内)** | **保留或收敛导出**:仅 `senseTriggerLabels` 对外即可;`labelSenseTrigger` 可作为内部函数若不需要单独暴露。 |
|
||||
| `packages/core/src/index.ts` → `util.js` | `parseDurationStringToMs`(仅 `config.ts` 内部使用) | **高** | **调查**:无需在 `index` 再导出则改为非导出或移除 re-export,减少 API 面。 |
|
||||
| `packages/core/src/index.ts` → `sense.js` | 类型 `SenseModule` | **中** | **保留**:用户文档/外部 Sense 模块常用;仓内未按名引用属正常。 |
|
||||
| `packages/core/src/index.ts` → `config.js` | 单独导出的类型 `NerveApiConfig`(仅 `config.ts` 与 `index` 出现) | **低** | **保留**:通常随 `NerveConfig` 一并使用;单独 export 多为 TS 公共类型便利。 |
|
||||
|
||||
### 1.2 `@uncaged/nerve-workflow-utils`
|
||||
|
||||
下列项在 **`packages/*/src/**/*.ts` 中无人从包入口导入**(`workflow-meta`、`role-committer` 等主要只用 `createRole`、`decorateRole`、`onFail`、`withDryRun`、`LlmExtractorConfig`)。
|
||||
|
||||
| 路径 | 未使用项 | 置信度 | 建议 |
|
||||
|------|----------|--------|------|
|
||||
| `packages/workflow-utils/src/index.ts` | `createLlmAdapter` | **高** | **保留(对外 API)** 或标注文档;仓内无调用。 |
|
||||
| 同上 | `llmExtract`、`llmExtractWithRetry` | **高** | **保留**:高级用法 / 文档示例;内部与测试使用。 |
|
||||
| 同上 | `mergeExtractConfig`、`ExtractConfigLayer` | **高** | **调查**:若 RFC 分层配置仍在推进则保留;否则评估移除导出。 |
|
||||
| 同上 | `assertZodMetaSchemas`、`createLlmExtractFn`、`extractMetaOrThrow`、`zodMeta`、`ZodMetaSchema` | **高** | **保留(对外 API)**;当前仅 `workflow-utils` 测试与内部 `create-role` 链路使用 `extractMetaOrThrow`。 |
|
||||
| 同上 | `readNerveYaml`、`nerveAgentContext` 及相关类型 | **高** | **调查**:若已无 YAML 上下文注入场景可删导出;否则保留给 Agent 工具链。 |
|
||||
| 同上 | `spawnSafe`、`nerveCommandEnv` 及 `Spawn*` 类型(从 core 再导出) | **高** | **删除再导出或改为文档链接**:仓内均直接从 `@uncaged/nerve-core` 引用 spawn。 |
|
||||
| 同上 | `isDryRun` | **高** | **删除或实现**:见 §3「死函数」。 |
|
||||
| 同上 | `LlmMessage`、`MetaExtractConfig`、`LlmChatError`、`LlmError`、`LlmProvider` 等类型再导出 | **中** | **保留**:供外部精细类型标注;仓内未单独 import。 |
|
||||
|
||||
### 1.3 `@uncaged/nerve-daemon`
|
||||
|
||||
| 路径 | 未使用项 | 置信度 | 建议 |
|
||||
|------|----------|--------|------|
|
||||
| `packages/daemon/src/index.ts` → `agent-adapters/echo.js` | `createEchoAgent` | **高** | **调查**:无任何测试或运行时引用;若设计保留 `type: "echo"` 适配器,应在 workflow/agent 装载路径接线或删除导出与文件。 |
|
||||
| `packages/daemon/src/index.ts`(及其它导出) | 多数 IPC / runtime 符号 | **低** | **保留**:动态加载与外部集成无法静态判定;仅 `createEchoAgent` 可确定为当前静态图下的死角。 |
|
||||
|
||||
### 1.4 `@uncaged/nerve-adapter-cursor` / `@uncaged/nerve-adapter-hermes`
|
||||
|
||||
| 路径 | 未使用项 | 置信度 | 建议 |
|
||||
|------|----------|--------|------|
|
||||
| `packages/adapter-cursor/src/index.ts` | `cursorAdapter`、`createCursorAdapter`(以及 `cursorAgent` 仅被 `workflow-utils/src/role-cursor.ts` 使用) | **中** | **保留**:默认实例与工厂供下游与工作流仓使用;仓内业务包未直接 import `cursorAdapter` 属预期。 |
|
||||
| `packages/adapter-hermes/src/index.ts` | `hermesAdapter`、`createHermesAdapter`(`hermesAgent` 经 `workflow-utils` 间接使用) | **中** | 同上。 |
|
||||
|
||||
### 1.5 `@uncaged/nerve-cli`
|
||||
|
||||
| 路径 | 未使用项 | 置信度 | 建议 |
|
||||
|------|----------|--------|------|
|
||||
| `packages/cli/src/index.ts` | `getNerveRoot`、`loadDaemonModule` 等程序化导出 | **高(仓内)** | **保留**:无任何 workspace 包引用 `@uncaged/nerve-cli`;面向 CLI 二次开发者。 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 孤儿文件(非入口、未被其它源码引用)
|
||||
|
||||
在当前 Rslib / `cli` 入口定义下,**未发现**「零 import、且非 entry / 非测试」的 `.ts` 业务文件:
|
||||
|
||||
- `workflow-utils` 下的 `role-cursor.ts`、`role-hermes.ts`、`role-llm.ts`、`role-react.ts` 虽**未出现在包 `index.ts`**,但被 `packages/workflow-utils/src/__tests__/role-factories.test.ts` 直接引用,**不是孤儿文件**。
|
||||
- `daemon` 的 `sense-worker.ts`、`workflow-worker.ts` 等为 **显式构建入口**。
|
||||
|
||||
**置信度**: 对全表扫描为 **中**(未跑依赖图工具;动态路径可能掩盖极少数脚本引用)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 死函数(内部未导出且未被调用)
|
||||
|
||||
| 路径 | 说明 | 置信度 | 建议 |
|
||||
|------|------|--------|------|
|
||||
| `packages/workflow-utils/src/role-types.ts` | `isDryRun(_start: StartStep)`:导出在 `index.ts`,但全仓无调用(函数体恒返回 `false` 且标注 deprecated) | **高** | **删除**或改为内部常量;若需向后兼容则保留并文档标注「保留占位」。 |
|
||||
|
||||
其它内部函数未系统逐文件枚举。
|
||||
|
||||
---
|
||||
|
||||
## 4. 未使用的 npm 依赖(package.json)
|
||||
|
||||
在声明范围内通过源码 `import` 抽查:**未发现明显「完全未使用」的生产依赖**。
|
||||
|
||||
| 包 | 依赖 | 结论 |
|
||||
|----|------|------|
|
||||
| `cli` | `citty`、`yaml`、`picomatch` | 均有对应源码引用。 |
|
||||
| `khala` | `hono`、`jsonata`、`ulidx`、`@uncaged/nerve-core` | 均有引用。 |
|
||||
| `core` / `daemon` | `yaml`、`drizzle-orm` | 均有引用。 |
|
||||
| `workflow-utils` / `workflow-meta` | `zod`、workspace 包 | 均有引用。 |
|
||||
|
||||
**置信度**: **中**(未对 peer、可选路径、CLI `bin` 专用依赖做自动化扫描)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 陈旧测试夹具 / 未引用辅助文件
|
||||
|
||||
未发现独立的「fixture 目录」明显失联;`cli` 下 `e2e-harness`、`__tests__` 内 helper 均有对应测试引用。
|
||||
|
||||
**置信度**: **低**(未枚举每个 `__tests__` 资源文件)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 重构遗留 / 文档漂移
|
||||
|
||||
| 项目 | 位置 | 说明 | 置信度 | 建议 |
|
||||
|------|------|------|--------|------|
|
||||
| 已更名 API 仍出现在 README | `packages/core/README.md` | 仍描述 `parseSenseWorkflowDirective`、`ParsedSenseWorkflowDirective`、`SenseComputeRoute`;源码已为 `parseWorkflowTrigger` / `routeSenseComputeOutput` / `RoutedSenseOutput` | **高** | **更新文档**(本次分析不改代码,仅记录)。 |
|
||||
| Hermes 选项合并注释 | `packages/workflow-utils/src/shared/hermes-agent.ts` | 注释称 absorbed from `hermes-options.ts`,该文件已不存在 | **中** | **清理注释**,避免误导。 |
|
||||
| `KNOWN_AGENT_ADAPTER_IDS` 含 `codex` | `packages/core/src/agent.ts` | 仓内无 `codex` 适配器包;与常量未被引用叠加 | **中** | **对齐产品**:实现适配器或从列表移除。 |
|
||||
|
||||
未发现用户提到的 `worker-fork-support` 等字符串副本(全仓无匹配)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 汇总建议优先级
|
||||
|
||||
1. **高优先级调查**: `createEchoAgent` 与 `KNOWN_AGENT_ADAPTER_IDS` — 要么接入运行时,要么删减以免维护假象。
|
||||
2. **API 面收敛**: `parseDurationStringToMs`、`labelSenseTrigger` 若无意对外,可从 `core` 公共导出移除。
|
||||
3. **`workflow-utils`**: 评估 `isDryRun` 删除;`spawnSafe` 等从 `workflow-utils` 再导出是否仍有必要。
|
||||
4. **文档**: 修正 `packages/core/README.md` 中 Sense→Workflow 路由 API 名称。
|
||||
|
||||
---
|
||||
|
||||
## 8. 重新验证命令示例
|
||||
|
||||
后续可在本地采用工具增强置信度(非本次执行):
|
||||
|
||||
```bash
|
||||
pnpm add -D -w knip
|
||||
pnpm exec knip
|
||||
```
|
||||
|
||||
或在各包对 `@uncaged/nerve-*` 的导出做面向消费者的契约测试,避免误删对外 API。
|
||||
@@ -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/
|
||||
│ └── <name>/
|
||||
│ ├── src/index.ts # exports compute() + table
|
||||
│ ├── src/schema.ts # drizzle 表定义
|
||||
│ ├── src/schema.ts # Drizzle 表定义
|
||||
│ └── migrations/ # SQL 迁移
|
||||
├── workflows/
|
||||
│ └── <name>/
|
||||
│ ├── index.ts # exports WorkflowDefinition
|
||||
│ └── roles/<role>/
|
||||
│ ├── index.ts # role 实现
|
||||
│ └── prompt.md # 可选 system prompt
|
||||
│ ├── index.ts # default export:WorkflowDefinition
|
||||
│ ├── moderator.ts # 可选:抽出 moderator,由 index 导入
|
||||
│ ├── build.ts # 可选:共享常量 / 纯函数(避免 index 臃肿;非 esbuild 入口)
|
||||
│ └── roles/
|
||||
│ └── <role>.ts # 每角色单文件(推荐平铺,而非 roles/<role>/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 <name>
|
||||
nerve remote default <name> # 设为默认远程
|
||||
```
|
||||
|
||||
### Agent(向 Hermes 注入本 skill)
|
||||
|
||||
```bash
|
||||
nerve agent status # CLI 版本与各 Hermes 注入目录中的 skill 版本
|
||||
nerve agent inject hermes # 安装到 ~/.hermes/skills/nerve
|
||||
nerve agent inject hermes --profile <name> # 写入 ~/.hermes/profiles/<name>/skills/nerve
|
||||
nerve agent update # 将所有已注入目录更新到当前 CLI 对应版本
|
||||
nerve agent remove hermes # 移除默认 profile 的注入
|
||||
nerve agent remove hermes --profile <name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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<string, LibSQLDatabase>, // 其他 sense 的数据库(只读)
|
||||
options: { signal: AbortSignal }, // 超时 abort signal
|
||||
): Promise<ComputeResult<T>>
|
||||
```typescript
|
||||
import type { ComputeResult, SenseComputeFn } from "@uncaged/nerve-core";
|
||||
|
||||
export const compute: SenseComputeFn<MySignalShape> = async () => {
|
||||
// ...
|
||||
};
|
||||
// 或等价地:
|
||||
export async function compute(): Promise<ComputeResult<MySignalShape>> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
(运行时定义见 `@uncaged/nerve-core` 的 `SenseComputeFn` / `SenseModule`,daemon 侧在 `sense-runtime.ts` 的 `executeCompute` 中插入 `result.signal`。)
|
||||
|
||||
### 返回值
|
||||
|
||||
```typescript
|
||||
// 返回 null = 静默,不发 signal
|
||||
// 返回非 null = 发出 signal,可选触发 workflow
|
||||
// 返回非 null = 发出 signal(并写入业务表),可选触发 workflow
|
||||
type ComputeResult<T> =
|
||||
| null
|
||||
| { signal: T; workflow: WorkflowTrigger | null };
|
||||
@@ -233,21 +260,20 @@ type WorkflowTrigger = {
|
||||
};
|
||||
```
|
||||
|
||||
若返回值是普通对象且不含 `signal` 字段,内核会按 shorthand 视为 `{ signal: payload, workflow: null }`(见 core 的 `routeSenseComputeOutput`)。
|
||||
|
||||
### Sense 模块导出
|
||||
|
||||
```typescript
|
||||
// senses/<name>/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<string, LibSQLDatabase>,
|
||||
_options: { signal: AbortSignal },
|
||||
): Promise<ComputeResult<number>> {
|
||||
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<ComputeResult<Row>> {
|
||||
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<string, LibSQLDatabase>,
|
||||
_options: { signal: AbortSignal },
|
||||
): Promise<ComputeResult<number>> {
|
||||
type Row = { ts: number; value: number };
|
||||
|
||||
export async function compute(): Promise<ComputeResult<Row>> {
|
||||
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<Meta> = (ctx: ThreadContext) => Promise<RoleResult<Meta>>;
|
||||
type RoleResult<Meta> = { content: string; meta: Meta };
|
||||
|
||||
// Moderator:路由器,决定下一个 role 或结束
|
||||
type Moderator<M> = (ctx: ThreadContext<M>) => (keyof M & string) | typeof END;
|
||||
|
||||
// ThreadContext:对话上下文
|
||||
type ThreadContext<M = RoleMeta> = {
|
||||
threadId: string;
|
||||
start: StartStep; // 初始 prompt(role: "__start__")
|
||||
steps: RoleStep<M>[]; // 所有 role 的执行记录
|
||||
};
|
||||
|
||||
// WorkflowDefinition:完整定义
|
||||
type WorkflowDefinition<M> = {
|
||||
name: string;
|
||||
roles: { [K in keyof M & string]: Role<M[K]> };
|
||||
moderator: Moderator<M>;
|
||||
};
|
||||
// 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
|
||||
```
|
||||
|
||||
### 基本 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<string>`(原始模型输出文本)。
|
||||
2. **prompt** — `string`,或 `async (ctx: ThreadContext) => string`。
|
||||
3. **meta** — `z.ZodType<M>`,供 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<RoleResult<{ round: number }>> {
|
||||
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`)
|
||||
|
||||
```typescript
|
||||
// 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 },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```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<Meta> = {
|
||||
name: "example",
|
||||
roles: { main },
|
||||
moderator(ctx: ThreadContext<Meta>) {
|
||||
// 执行一次 main 就结束
|
||||
return ctx.steps.length === 0 ? "main" : END;
|
||||
},
|
||||
};
|
||||
@@ -387,25 +434,50 @@ const workflow: WorkflowDefinition<Meta> = {
|
||||
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<RoleResult<{ status: string }>> {
|
||||
export async function planner(ctx: ThreadContext): Promise<RoleResult<{ status: string }>> {
|
||||
void ctx;
|
||||
return { content: "计划: ...", meta: { status: "planned" } };
|
||||
}
|
||||
```
|
||||
|
||||
async function executor(ctx: ThreadContext): Promise<RoleResult<{ status: string }>> {
|
||||
```typescript
|
||||
// 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" } };
|
||||
}
|
||||
```
|
||||
|
||||
async function reviewer(ctx: ThreadContext): Promise<RoleResult<{ status: string }>> {
|
||||
```typescript
|
||||
// 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" } };
|
||||
}
|
||||
```
|
||||
|
||||
```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<Roles> = {
|
||||
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<string>;
|
||||
```
|
||||
|
||||
没有现成 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/<name>/ 子目录)
|
||||
# 推荐对齐 AGENT.md:workflows/my-flow/index.ts + roles/<role>.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 <runId> # 查看执行轨迹
|
||||
```
|
||||
@@ -496,10 +571,10 @@ nerve thread show <runId> # 查看执行轨迹
|
||||
## 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/<name>/index.ts` 使用 default export(daemon 加载约定)。
|
||||
- **_signals 表**:每个 sense 自动有 `_signals` 表记录 signal 历史,受 `retention` 配置限制。
|
||||
- **peers 只读**:sense 的 `peers` 参数提供其他 sense 数据库的只读访问,不要写入。
|
||||
- **concurrency + overflow**:workflow 必须配置并发策略,否则验证失败。
|
||||
- **moderator 是同步函数**:不要加 async,moderator 是纯路由逻辑,不能有副作用。
|
||||
|
||||
+22
-14
@@ -4,11 +4,11 @@ Shared types and configuration parser for the [nerve](../../README.md) observati
|
||||
|
||||
## What's Inside
|
||||
|
||||
- **Type definitions** — `Signal`, `SenseConfig`, `SenseInfo`, `SenseReflexConfig`, `ReflexConfig` (sense-only), `WorkflowConfig`, `NerveConfig`, and related types
|
||||
- **Type definitions** — `Signal`, `SenseConfig`, `SenseInfo`, `WorkflowConfig`, `NerveConfig`, and related types
|
||||
- **Config parser** — `parseNerveConfig(yaml)` validates and parses `nerve.yaml` into `NerveConfig` (rejects reflex entries that declare a `workflow` key; reflexes only schedule senses)
|
||||
- **Sense → workflow routing** — `parseSenseWorkflowDirective`, `routeSenseComputeOutput`, and types `ParsedSenseWorkflowDirective`, `SenseComputeRoute`
|
||||
- **Sense → workflow routing** — `parseWorkflowTrigger`, `routeSenseComputeOutput`, and types `WorkflowTrigger`, `RoutedSenseOutput`
|
||||
- **Daemon IPC protocol** — request/response types (`DaemonIpcRequest`, `DaemonIpcResponse`, …) and `parseDaemonIpcRequest` for newline-delimited JSON on the CLI ↔ daemon socket
|
||||
- **Workflow automaton types** — `START` / `END` sentinel constants, `WorkflowMessage`, `StartStep`, `RoleStep`, `ModeratorContext` (`start` + `steps`; empty `steps` on first moderator call), `Moderator` (single `context` argument), `WorkflowDefinition`, `Role`, `SenseResult`, plus `DEFAULT_ENGINE_MAX_ROUNDS`
|
||||
- **Workflow automaton types** — `START` / `END` sentinel constants, `WorkflowMessage`, `StartStep`, `RoleStep`, `ModeratorContext` (`start` + `steps`; empty `steps` on first moderator call), `Moderator` (single `context` argument), `WorkflowDefinition`, `Role`, `RoleResult`, plus `DEFAULT_ENGINE_MAX_ROUNDS`
|
||||
- **Result type** — `Result<T>` with `ok()` / `err()` helpers for explicit error handling (no thrown exceptions for parse paths)
|
||||
|
||||
## Usage
|
||||
@@ -26,23 +26,31 @@ if (result.ok) {
|
||||
### Sense return → signal vs workflow
|
||||
|
||||
```typescript
|
||||
import { parseSenseWorkflowDirective, routeSenseComputeOutput } from "@uncaged/nerve-core";
|
||||
import { parseWorkflowTrigger, routeSenseComputeOutput } from "@uncaged/nerve-core";
|
||||
|
||||
const directive = parseSenseWorkflowDirective("my-workflow|8|Hello from sense");
|
||||
const directive = parseWorkflowTrigger({
|
||||
name: "my-workflow",
|
||||
maxRounds: 8,
|
||||
prompt: "Hello from sense",
|
||||
dryRun: false,
|
||||
});
|
||||
if (directive.ok) {
|
||||
console.log(directive.value.workflowName, directive.value.maxRounds, directive.value.prompt);
|
||||
console.log(directive.value.name, directive.value.maxRounds, directive.value.prompt);
|
||||
}
|
||||
|
||||
const route = routeSenseComputeOutput({
|
||||
metric: 42,
|
||||
workflow: "my-workflow|8|Run now",
|
||||
signal: { metric: 42 },
|
||||
workflow: {
|
||||
name: "my-workflow",
|
||||
maxRounds: 8,
|
||||
prompt: "Run now",
|
||||
dryRun: false,
|
||||
},
|
||||
});
|
||||
if (route.kind === "launch") {
|
||||
// engine starts workflow; no Signal to the bus for this return
|
||||
console.log(route.launch);
|
||||
} else {
|
||||
// normal signal with payload
|
||||
console.log(route.payload);
|
||||
if (route.ok && route.value.workflow !== null) {
|
||||
console.log(route.value.workflow);
|
||||
} else if (route.ok) {
|
||||
console.log(route.value.signal);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -21,9 +21,3 @@ export class ExtractError extends Error {
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent adapter ids referenced by tooling / docs (RFC-003).
|
||||
* Workflows import adapter packages directly; echo may be used in tests via a small factory.
|
||||
*/
|
||||
export const KNOWN_AGENT_ADAPTER_IDS = ["echo", "cursor", "hermes", "codex"] as const;
|
||||
|
||||
@@ -13,7 +13,7 @@ export type {
|
||||
} from "./config.js";
|
||||
export type { Signal, SenseInfo } from "./sense.js";
|
||||
export type { SenseComputeFn, SenseModule } from "./sense.js";
|
||||
export { labelSenseTrigger, senseTriggerLabels } from "./sense.js";
|
||||
export { senseTriggerLabels } from "./sense.js";
|
||||
export type {
|
||||
WorkflowMessage,
|
||||
RoleResult,
|
||||
@@ -29,7 +29,6 @@ export type {
|
||||
WorkflowDefinition,
|
||||
} from "./workflow.js";
|
||||
export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "./workflow.js";
|
||||
export { parseDurationStringToMs } from "./util.js";
|
||||
export type { Schema, ExtractFn } from "./agent.js";
|
||||
export { ExtractError } from "./agent.js";
|
||||
export type { Result } from "./util.js";
|
||||
@@ -46,7 +45,6 @@ export { parseNerveConfig } from "./config.js";
|
||||
export type { KnowledgeConfig } from "./config.js";
|
||||
export { parseKnowledgeYaml } from "./config.js";
|
||||
export { isPlainRecord } from "./util.js";
|
||||
export { KNOWN_AGENT_ADAPTER_IDS } from "./agent.js";
|
||||
|
||||
export type { RoutedSenseOutput } from "./sense.js";
|
||||
export { parseWorkflowTrigger, routeSenseComputeOutput } from "./sense.js";
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { AgentConfig, AgentFn, ThreadContext } from "@uncaged/nerve-core";
|
||||
|
||||
/**
|
||||
* Echo adapter (`type: "echo"`) — returns the assembled prompt unchanged.
|
||||
* Used for tests and dry-run wiring before real adapters exist.
|
||||
*/
|
||||
export function createEchoAgent(_config: AgentConfig): AgentFn {
|
||||
return async (_ctx: ThreadContext, prompt: string) => prompt;
|
||||
}
|
||||
@@ -56,5 +56,3 @@ export type {
|
||||
|
||||
export { createWorkflowManager } from "./workflow-manager.js";
|
||||
export type { WorkflowManager } from "./workflow-manager.js";
|
||||
|
||||
export { createEchoAgent } from "./agent-adapters/echo.js";
|
||||
|
||||
@@ -26,13 +26,11 @@ export {
|
||||
} from "./role-decorators.js";
|
||||
export {
|
||||
nerveCommandEnv,
|
||||
spawnSafe,
|
||||
type SpawnEnv,
|
||||
type SpawnError,
|
||||
type SpawnResult,
|
||||
type SpawnSafeOptions,
|
||||
} from "@uncaged/nerve-core";
|
||||
export type { LlmError, LlmProvider } from "./shared/llm-extract.js";
|
||||
export { isDryRun } from "./role-types.js";
|
||||
export type { LlmMessage, MetaExtractConfig } from "./role-types.js";
|
||||
export type { LlmChatError } from "./shared/llm-chat.js";
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import type { SpawnEnv, StartStep, ThreadContext } from "@uncaged/nerve-core";
|
||||
import type { SpawnEnv, ThreadContext } from "@uncaged/nerve-core";
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { LlmProvider } from "./shared/llm-extract.js";
|
||||
|
||||
/**
|
||||
* @deprecated `dryRun` has been removed from `StartStep.meta` (RFC-005).
|
||||
* Use adapter/role-level `dryRun` config instead.
|
||||
*/
|
||||
export function isDryRun(_start: StartStep): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export type CliPromptFn = (ctx: ThreadContext) => Promise<string>;
|
||||
|
||||
export type LlmMessage = { role: "system" | "user" | "assistant"; content: string };
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { HermesRoleDefaults, HermesRoleRequired } from "../role-types.js";
|
||||
export { hermesAgent } from "@uncaged/nerve-adapter-hermes";
|
||||
export type { HermesAgentOptions } from "@uncaged/nerve-adapter-hermes";
|
||||
|
||||
// --- Hermes options resolution (absorbed from hermes-options.ts) ---
|
||||
// --- Hermes role defaults resolution ---
|
||||
|
||||
const HERMES_DEFAULTS: HermesRoleDefaults = {
|
||||
model: null,
|
||||
|
||||
Reference in New Issue
Block a user