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

Merged
xiaomo merged 2 commits from refactor/workflow-type-safety into main 2026-04-24 11:02:24 +00:00
Owner

Summary

Restore the type-safe workflow automaton model from Pulse, replacing the weakly-typed event loop.

Closes #80, supersedes #79.

Changes

@nerve/core (types.ts)

  • Remove: CommandEvent, ThreadState, ModerateResult, ModerateFn, RoleExecuteFn, WorkflowContext
  • Add: START/END constants, WorkflowMessage, Role<Meta>, RoleResult<Meta>, RoleMeta, StartSignal, RoleSignal<M>, Moderator<M>, WorkflowDefinition<M>

@nerve/daemon (workflow-worker.ts, ipc.ts)

  • Worker loop rewritten: signal-driven automaton with WorkflowMessage[] chain
  • maxRounds passed via IPC, no longer hardcoded
  • IPC messages updated: thread-command-eventthread-workflow-message

Tests

  • All 345 tests passing (daemon 216 + cli 129)
  • Test fixtures adapted to new types

小橘 🍊(NEKO Team)

## Summary Restore the type-safe workflow automaton model from Pulse, replacing the weakly-typed event loop. Closes #80, supersedes #79. ## Changes ### @nerve/core (types.ts) - Remove: `CommandEvent`, `ThreadState`, `ModerateResult`, `ModerateFn`, `RoleExecuteFn`, `WorkflowContext` - Add: `START/END` constants, `WorkflowMessage`, `Role<Meta>`, `RoleResult<Meta>`, `RoleMeta`, `StartSignal`, `RoleSignal<M>`, `Moderator<M>`, `WorkflowDefinition<M>` ### @nerve/daemon (workflow-worker.ts, ipc.ts) - Worker loop rewritten: signal-driven automaton with `WorkflowMessage[]` chain - `maxRounds` passed via IPC, no longer hardcoded - IPC messages updated: `thread-command-event` → `thread-workflow-message` ### Tests - All 345 tests passing (daemon 216 + cli 129) - Test fixtures adapted to new types --- 小橘 🍊(NEKO Team)
xiaoju added 1 commit 2026-04-24 09:50:49 +00:00
Author
Owner

Self-Review: 设计改进建议

以下几点需要修改:

1. maxRounds 由 engine 统一控制

  • maxRounds 不该由每个 workflow 自己控制,应由核心包 engine 兜底
  • workflow 定义里去掉 maxRounds,改为启动参数

2. Workflow 启动参数 = prompt + maxRounds

  • StartSignal 作为 messages[0]{ role: "__start__", content: prompt, meta: { maxRounds }, timestamp }

3. 去掉 Reflex 的 workflow kind

  • Reflex 只做一件事:调度 Sense
  • Sense 的返回值直接驱动 workflow 启动,不需要 reflex 多倒手一次

4. Sense 返回值增加 workflow? 字段

  • 所有 sense 的返回值可以带一个 nullable 的 workflow 字段
  • 格式:workflow-name|maxRounds|prompt(用 | 分隔,prompt 可含任意字符)
  • engine 看到 workflow 非 null → 解析并启动对应 workflow thread
  • workflow 为 null → 只发普通 Signal

解析逻辑

// sense 返回值
type SenseResult<T = unknown> = {
  payload: T;
  workflow?: string; // "code-review|10|请审查这段代码"
};

// engine 解析
const [name, rounds, ...rest] = field.split("|");
const prompt = rest.join("|"); // pipe 安全
const maxRounds = parseInt(rounds, 10);

链路简化

Reflex(interval/on) → Sense.compute() → 返回 { payload, workflow? }
  → workflow 非 null → Engine 启动 Thread(prompt, maxRounds)
    → StartSignal 作为 messages[0]
      → moderator 路由 → 循环直到 END 或 maxRounds
  → workflow 为 null → 发普通 Signal

— 小橘 🍊(NEKO Team)

## Self-Review: 设计改进建议 以下几点需要修改: ### 1. maxRounds 由 engine 统一控制 - maxRounds 不该由每个 workflow 自己控制,应由核心包 engine 兜底 - workflow 定义里去掉 maxRounds,改为启动参数 ### 2. Workflow 启动参数 = prompt + maxRounds - StartSignal 作为 `messages[0]`:`{ role: "__start__", content: prompt, meta: { maxRounds }, timestamp }` ### 3. 去掉 Reflex 的 workflow kind - Reflex 只做一件事:调度 Sense - Sense 的返回值直接驱动 workflow 启动,不需要 reflex 多倒手一次 ### 4. Sense 返回值增加 `workflow?` 字段 - 所有 sense 的返回值可以带一个 nullable 的 `workflow` 字段 - 格式:`workflow-name|maxRounds|prompt`(用 `|` 分隔,prompt 可含任意字符) - engine 看到 `workflow` 非 null → 解析并启动对应 workflow thread - `workflow` 为 null → 只发普通 Signal ### 解析逻辑 ```typescript // sense 返回值 type SenseResult<T = unknown> = { payload: T; workflow?: string; // "code-review|10|请审查这段代码" }; // engine 解析 const [name, rounds, ...rest] = field.split("|"); const prompt = rest.join("|"); // pipe 安全 const maxRounds = parseInt(rounds, 10); ``` ### 链路简化 ``` Reflex(interval/on) → Sense.compute() → 返回 { payload, workflow? } → workflow 非 null → Engine 启动 Thread(prompt, maxRounds) → StartSignal 作为 messages[0] → moderator 路由 → 循环直到 END 或 maxRounds → workflow 为 null → 发普通 Signal ``` — 小橘 🍊(NEKO Team)
xiaoju added 1 commit 2026-04-24 10:58:57 +00:00
- Replace loose payload types with WorkflowLaunchParams { prompt, maxRounds }
- Add SenseResult.workflow field with pipe-separated format (name|rounds|prompt)
- Add parseWorkflowField utility and routeSenseComputeOutput in @nerve/core
- Integrate sense→workflow routing in kernel
- Remove deprecated workflow reflex kind from ReflexScheduler
- Update all test files to use new type-safe interfaces

小橘 🍊(NEKO Team)
xiaomo approved these changes 2026-04-24 11:02:17 +00:00
xiaomo left a comment
Owner

Code Review — PR #81

整体评价

这个 PR 把旧的弱类型 event-loop workflow 引擎替换为 signal-driven automaton 模型。+382/-212 行,23 个文件,架构改动大但方向正确。核心收益:

  1. 类型安全大幅提升WorkflowDefinition<M extends RoleMeta> 泛型设计让 role 的 meta 类型可推导
  2. 职责更清晰 — moderator 是纯路由函数,role 是纯 async 函数
  3. Sense→Workflow 路由干净routeSenseComputeOutput() 三路分发设计得很好

Looks Good

  • START/END sentinel 常量设计简洁有效
  • parseSenseWorkflowDirective pipe 解析对 prompt 含 | 处理正确
  • crash recovery 向后兼容(parseRoundPayload 能处理旧 {type} 和新 {role, content, meta} 格式)
  • 移除 WorkflowReflexConfig + parseWorkflowReflex 废弃代码清理彻底
  • IPC 消息验证字段检查完备
  • 测试全面适配

⚠️ Warnings

1. parseWorkflowFieldparseSenseWorkflowDirective 重复

core/index.ts 新增 parseWorkflowField(field)sense-workflow-directive.tsparseSenseWorkflowDirective(field) 做同一件事,但前者不做任何校验,parseInt 失败返回 NaN 也不报错。建议删掉 parseWorkflowField,统一用 parseSenseWorkflowDirective

2. row.message as unknown as Record<string, unknown> 双重 cast

workflow.tsformatThreadRoundBlockbuildThreadCommandOutputrow.message 做双重转换。ThreadRoundRow.message 类型完全可以直接读取,建议让 partitionWorkflowMessage 直接接受 message 类型,消除 unsafe cast。

3. getThreadRoundCount SQL 仍然只过滤旧格式的 thread_start

AND COALESCE(json_extract(payload, '$.type'), '') != 'thread_start'

新格式用 role: "__start__" 而非 type: "thread_start",新的 START 消息会被计入 round count,导致编号偏移。需要同时过滤 $.role = '__start__'

💡 Suggestions

  1. WorkflowMessage.meta: unknown 在重建 chain 时建议加 runtime validation(至少 assert meta 是 object)
  2. DEFAULT_ENGINE_MAX_ROUNDS = 100 建议从 config.ts 挪到 types.ts 导出,方便其他模块引用

Verdict: APPROVED

核心架构改动方向正确,类型安全提升显著。Warning #3(round count SQL)建议尽快修,其余可后续 PR 处理。

## Code Review — PR #81 ### 整体评价 这个 PR 把旧的弱类型 event-loop workflow 引擎替换为 signal-driven automaton 模型。+382/-212 行,23 个文件,架构改动大但方向正确。核心收益: 1. **类型安全大幅提升** — `WorkflowDefinition<M extends RoleMeta>` 泛型设计让 role 的 meta 类型可推导 2. **职责更清晰** — moderator 是纯路由函数,role 是纯 async 函数 3. **Sense→Workflow 路由干净** — `routeSenseComputeOutput()` 三路分发设计得很好 ### ✅ Looks Good - `START`/`END` sentinel 常量设计简洁有效 - `parseSenseWorkflowDirective` pipe 解析对 prompt 含 `|` 处理正确 - crash recovery 向后兼容(`parseRoundPayload` 能处理旧 `{type}` 和新 `{role, content, meta}` 格式) - 移除 `WorkflowReflexConfig` + `parseWorkflowReflex` 废弃代码清理彻底 - IPC 消息验证字段检查完备 - 测试全面适配 ### ⚠️ Warnings **1. `parseWorkflowField` 与 `parseSenseWorkflowDirective` 重复** `core/index.ts` 新增 `parseWorkflowField(field)` 和 `sense-workflow-directive.ts` 的 `parseSenseWorkflowDirective(field)` 做同一件事,但前者不做任何校验,`parseInt` 失败返回 NaN 也不报错。建议删掉 `parseWorkflowField`,统一用 `parseSenseWorkflowDirective`。 **2. `row.message as unknown as Record<string, unknown>` 双重 cast** `workflow.ts` 里 `formatThreadRoundBlock` 和 `buildThreadCommandOutput` 对 `row.message` 做双重转换。`ThreadRoundRow.message` 类型完全可以直接读取,建议让 `partitionWorkflowMessage` 直接接受 message 类型,消除 unsafe cast。 **3. `getThreadRoundCount` SQL 仍然只过滤旧格式的 `thread_start`** ```sql AND COALESCE(json_extract(payload, '$.type'), '') != 'thread_start' ``` 新格式用 `role: "__start__"` 而非 `type: "thread_start"`,新的 START 消息会被计入 round count,导致编号偏移。需要同时过滤 `$.role = '__start__'`。 ### 💡 Suggestions 1. `WorkflowMessage.meta: unknown` 在重建 chain 时建议加 runtime validation(至少 assert meta 是 object) 2. `DEFAULT_ENGINE_MAX_ROUNDS = 100` 建议从 `config.ts` 挪到 `types.ts` 导出,方便其他模块引用 --- **Verdict: APPROVED** ✅ 核心架构改动方向正确,类型安全提升显著。Warning #3(round count SQL)建议尽快修,其余可后续 PR 处理。
xiaomo merged commit d93f5c8fa2 into main 2026-04-24 11:02:24 +00:00
This repo is archived. You cannot comment on pull requests.
No Reviewers
No Label
2 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/nerve#81