refactor(core): restore type-safe workflow automaton from Pulse design #80

Closed
opened 2026-04-24 09:34:28 +00:00 by xiaoju · 0 comments
Owner

Background

Nerve 的 workflow 类型系统相比 Pulse(packages/pulse/src/workflows/workflow-type.ts)严重退化:

维度 Pulse Nerve 当前
Role 泛型 Role<Meta> 类型安全 Role 无泛型,execute(prompt: unknown)
Moderator 输入 discriminated union,switch narrow CommandEvent = { type: string; [key: string]: unknown } 万金油
消息链 Role 接收 WorkflowMessage[] 完整历史 Role 只拿到 prompt: unknown,依赖 moderator 提炼
自动机边界 START/END 伪角色 魔法字符串 "thread_start"
安全阀 limits.maxRounds 类型内置 硬编码 MAX_STEPS = 1000 在 worker 里
content/meta 分离 content 存 CAS,meta 做路由 不区分,全塞 CommandEvent

核心问题:Pulse 精心设计的类型安全自动机模型,在 Nerve 里被简化成了无类型的事件循环。

Target Types

以下是讨论定稿的类型设计(@nerve/core):

const START = "__start__" as const;
const END = "__end__" as const;
type START = typeof START;
type END = typeof END;

// ── Chain(运行时,类型擦除)──
type WorkflowMessage = {
  role: string;
  content: string;
  meta: unknown;
  timestamp: number;
};

// ── Role(只关心自己的输出)──
type RoleResult<Meta> = { content: string; meta: Meta };
type Role<Meta> = (messages: WorkflowMessage[]) => Promise<RoleResult<Meta>>;

// ── Meta 映射表 ──
type RoleMeta = Record<string, Record<string, unknown>>;

// ── Signals(类型安全,给 moderator)──
type StartSignal = { role: START; meta: { maxRounds: number } };
type RoleSignal<M extends RoleMeta> = {
  [K in keyof M & string]: { role: K; meta: M[K] };
}[keyof M & string];

// ── Moderator & Workflow ──
type Moderator<M extends RoleMeta> = (
  signal: StartSignal | RoleSignal<M>,
  round: number,
  maxRounds: number,
) => (keyof M & string) | END;

type WorkflowDefinition<M extends RoleMeta> = {
  name: string;
  roles: { [K in keyof M & string]: Role<M[K]> };
  moderator: Moderator<M>;
};

Design Decisions

1. 一个泛型参数 M extends RoleMeta 驱动全部推导

用户声明一张 meta 映射表,类型系统自动对齐 roles 的 key、meta 类型、moderator 的 discriminated union:

type MyMeta = {
  coder: { files: string[] };
  reviewer: { approved: boolean };
};
const workflow: WorkflowDefinition<MyMeta> = { ... };

2. WorkflowMessage.meta 是 unknown

消息链是运行时数据,混合不同 role 的消息,静态类型推不动。Role 主要消费 content,需要 meta 时自行 assert。精确的 meta 类型留给 moderator 的 RoleSignal<M>

3. maxRounds 是 Thread 属性,不是 Workflow 属性

  • StartSignal.meta.maxRounds 传入
  • moderator 参数里拿到 round + maxRounds
  • WorkflowDefinition 本身不声明 maxRounds

4. START/END 用字符串常量,不用 Symbol

Nerve 用 IPC worker 进程隔离,Symbol 跨进程不可序列化。"__start__" / "__end__" 作为 as const 字符串常量。

5. Role 是纯函数,不限于 agent

Role 只是 (messages) => Promise<RoleResult<Meta>>。实现可以是:

  • 🤖 Agent(cursor-agent, claude 等)
  • 💬 单轮 LLM API 调用
  • 📜 普通脚本 / 计算函数
  • 🔗 HTTP 请求 / API 调用

类型不约束实现方式,只约束输入输出契约。

Scope

Phase 1: 类型替换(@nerve/core

  • 替换 types.ts 中 workflow 相关类型为上述定义
  • 移除 CommandEventThreadStateModerateResultModerateFnRoleExecuteFn 旧类型
  • 导出新类型:WorkflowMessageRoleRoleResultRoleMetaStartSignalRoleSignalModeratorWorkflowDefinitionSTARTEND

Phase 2: Worker 适配(@nerve/daemon

  • workflow-worker.ts 适配新类型:维护 WorkflowMessage[] chain,循环改为 signal 驱动
  • maxRounds 从 start-thread IPC 消息传入,不硬编码
  • IPC 消息类型更新

Phase 3: 防注入(#79 合并进来)

  • spawnSafe() 工具函数 — shell: false
  • Role 实现中 CLI 调用使用 argv 数组,不拼接 shell 字符串

Phase 4: 测试

  • 类型测试:WorkflowDefinition<M> 的类型推导正确性
  • 单元测试:moderator 路由、role 执行、chain 累积
  • 集成测试:完整 workflow 跑通(START → roles → END)

Supersedes

  • #79(shell 注入防护)— 合并为 Phase 3

小橘 🍊(NEKO Team)

## Background Nerve 的 workflow 类型系统相比 Pulse(`packages/pulse/src/workflows/workflow-type.ts`)严重退化: | 维度 | Pulse | Nerve 当前 | |------|-------|------------| | Role 泛型 | `Role<Meta>` 类型安全 | `Role` 无泛型,`execute(prompt: unknown)` | | Moderator 输入 | discriminated union,switch narrow | `CommandEvent = { type: string; [key: string]: unknown }` 万金油 | | 消息链 | Role 接收 `WorkflowMessage[]` 完整历史 | Role 只拿到 `prompt: unknown`,依赖 moderator 提炼 | | 自动机边界 | `START/END` 伪角色 | 魔法字符串 `"thread_start"` | | 安全阀 | `limits.maxRounds` 类型内置 | 硬编码 `MAX_STEPS = 1000` 在 worker 里 | | content/meta 分离 | content 存 CAS,meta 做路由 | 不区分,全塞 `CommandEvent` | 核心问题:**Pulse 精心设计的类型安全自动机模型,在 Nerve 里被简化成了无类型的事件循环。** ## Target Types 以下是讨论定稿的类型设计(`@nerve/core`): ```typescript const START = "__start__" as const; const END = "__end__" as const; type START = typeof START; type END = typeof END; // ── Chain(运行时,类型擦除)── type WorkflowMessage = { role: string; content: string; meta: unknown; timestamp: number; }; // ── Role(只关心自己的输出)── type RoleResult<Meta> = { content: string; meta: Meta }; type Role<Meta> = (messages: WorkflowMessage[]) => Promise<RoleResult<Meta>>; // ── Meta 映射表 ── type RoleMeta = Record<string, Record<string, unknown>>; // ── Signals(类型安全,给 moderator)── type StartSignal = { role: START; meta: { maxRounds: number } }; type RoleSignal<M extends RoleMeta> = { [K in keyof M & string]: { role: K; meta: M[K] }; }[keyof M & string]; // ── Moderator & Workflow ── type Moderator<M extends RoleMeta> = ( signal: StartSignal | RoleSignal<M>, round: number, maxRounds: number, ) => (keyof M & string) | END; type WorkflowDefinition<M extends RoleMeta> = { name: string; roles: { [K in keyof M & string]: Role<M[K]> }; moderator: Moderator<M>; }; ``` ## Design Decisions ### 1. 一个泛型参数 `M extends RoleMeta` 驱动全部推导 用户声明一张 meta 映射表,类型系统自动对齐 roles 的 key、meta 类型、moderator 的 discriminated union: ```typescript type MyMeta = { coder: { files: string[] }; reviewer: { approved: boolean }; }; const workflow: WorkflowDefinition<MyMeta> = { ... }; ``` ### 2. WorkflowMessage.meta 是 `unknown` 消息链是运行时数据,混合不同 role 的消息,静态类型推不动。Role 主要消费 `content`,需要 meta 时自行 assert。精确的 meta 类型留给 moderator 的 `RoleSignal<M>`。 ### 3. maxRounds 是 Thread 属性,不是 Workflow 属性 - `StartSignal.meta.maxRounds` 传入 - moderator 参数里拿到 `round` + `maxRounds` - WorkflowDefinition 本身不声明 maxRounds ### 4. START/END 用字符串常量,不用 Symbol Nerve 用 IPC worker 进程隔离,Symbol 跨进程不可序列化。`"__start__"` / `"__end__"` 作为 `as const` 字符串常量。 ### 5. Role 是纯函数,不限于 agent Role 只是 `(messages) => Promise<RoleResult<Meta>>`。实现可以是: - 🤖 Agent(cursor-agent, claude 等) - 💬 单轮 LLM API 调用 - 📜 普通脚本 / 计算函数 - 🔗 HTTP 请求 / API 调用 类型不约束实现方式,只约束输入输出契约。 ## Scope ### Phase 1: 类型替换(`@nerve/core`) - [ ] 替换 `types.ts` 中 workflow 相关类型为上述定义 - [ ] 移除 `CommandEvent`、`ThreadState`、`ModerateResult`、`ModerateFn`、`RoleExecuteFn` 旧类型 - [ ] 导出新类型:`WorkflowMessage`、`Role`、`RoleResult`、`RoleMeta`、`StartSignal`、`RoleSignal`、`Moderator`、`WorkflowDefinition`、`START`、`END` ### Phase 2: Worker 适配(`@nerve/daemon`) - [ ] `workflow-worker.ts` 适配新类型:维护 `WorkflowMessage[]` chain,循环改为 signal 驱动 - [ ] maxRounds 从 start-thread IPC 消息传入,不硬编码 - [ ] IPC 消息类型更新 ### Phase 3: 防注入(#79 合并进来) - [ ] `spawnSafe()` 工具函数 — `shell: false` - [ ] Role 实现中 CLI 调用使用 argv 数组,不拼接 shell 字符串 ### Phase 4: 测试 - [ ] 类型测试:`WorkflowDefinition<M>` 的类型推导正确性 - [ ] 单元测试:moderator 路由、role 执行、chain 累积 - [ ] 集成测试:完整 workflow 跑通(START → roles → END) ## Supersedes - #79(shell 注入防护)— 合并为 Phase 3 --- 小橘 🍊(NEKO Team)
This repo is archived. You cannot comment on issues.
No Label
1 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/nerve#80