refactor(core): remove WorkflowTrigger from SenseTrigger — shell only

Senses trigger shell commands only. Workflows are invoked via CLI.

SenseTrigger is now { command: string } — no discriminated union.

Closes #318

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-02 12:33:38 +00:00
parent 5ec0c71ee3
commit 8dd82d99da
22 changed files with 205 additions and 567 deletions
+7 -13
View File
@@ -212,10 +212,10 @@ extract:
### compute 函数签名
Sense 的 `compute` 接收当前状态,返回新状态和可选的 workflow trigger。状态以 JSON 文件持久化在 `data/senses/<name>.json`
Sense 的 `compute` 接收当前状态,返回新状态和可选的 shell trigger(`{ command: string }`。状态以 JSON 文件持久化在 `data/senses/<name>.json`Workflow 只能通过 CLI / daemon IPC 启动,不能从 sense 返回值直接启动。
```typescript
import type { SenseComputeFn, WorkflowTrigger } from "@uncaged/nerve-core";
import type { SenseComputeFn } from "@uncaged/nerve-core";
type MyState = {
lastRun: number | null;
@@ -226,7 +226,7 @@ export const initialState: MyState = { lastRun: null, count: 0 };
export async function compute(state: MyState): Promise<{
state: MyState;
trigger: WorkflowTrigger | null;
trigger: { command: string } | null;
}> {
return {
state: { lastRun: Date.now(), count: state.count + 1 },
@@ -247,15 +247,9 @@ export async function compute(state: MyState): Promise<{
### 返回值
```typescript
// trigger: null → 不触发 workflow
// trigger: WorkflowTrigger → 触发 workflow
type WorkflowTrigger = {
name: string; // workflow 名称(对应 nerve.yaml 中的 key)
maxRounds: number; // moderator 最大轮次
prompt: string; // 初始 prompt
dryRun: boolean; // 干跑模式
};
// trigger: null → 不执行 shell 命令
// trigger: { command } → sense worker 在成功的 compute 后以 shell 执行该命令(cwd = nerve 根目录)
// 启动 workflow:在 shell 中调用 `nerve workflow trigger ...`,或使用 daemon IPC / HTTP API
```
### Sense 模块导出
@@ -271,7 +265,7 @@ export const initialState: MyState = { ... };
// 2. compute 函数
export async function compute(state: MyState): Promise<{
state: MyState;
trigger: WorkflowTrigger | null;
trigger: { command: string } | null;
}> {
// ...
}
+1 -16
View File
@@ -252,22 +252,7 @@ export async function compute(): Promise<ComputeResult<MySignalShape>> {
### 返回值
```typescript
// 返回 null = 静默,不发 signal
// 返回非 null = 发出 signal(并写入业务表),可选触发 workflow
type ComputeResult<T> =
| null
| { signal: T; workflow: WorkflowTrigger | null };
type WorkflowTrigger = {
name: string; // workflow 名称(对应 nerve.yaml 中的 key)
maxRounds: number; // moderator 最大轮次
prompt: string; // 初始 prompt
dryRun: boolean; // 干跑模式
};
```
若返回值是普通对象且不含 `signal` 字段,内核会按 shorthand 视为 `{ signal: payload, workflow: null }`(见 core 的 `routeSenseComputeOutput`)。
当前引擎:`compute(state)` 返回 `{ state, trigger }`,`trigger` 为 `null` 或 `{ command: string }`(shell 命令)。Workflow 仅通过 CLI / daemon IPC 启动;类型见 `@uncaged/nerve-core` 的 `SenseComputeFn` / `SenseTrigger`。
### Sense 模块导出
+1 -16
View File
@@ -245,22 +245,7 @@ export async function compute(): Promise<ComputeResult<MySignalShape>> {
### 返回值
```typescript
// 返回 null = 静默,不发 signal
// 返回非 null = 发出 signal(并写入业务表),可选触发 workflow
type ComputeResult<T> =
| null
| { signal: T; trigger: WorkflowTrigger | null };
type WorkflowTrigger = {
name: string; // workflow 名称(对应 nerve.yaml 中的 key)
maxRounds: number; // moderator 最大轮次
prompt: string; // 初始 prompt
dryRun: boolean; // 干跑模式
};
```
若返回值是普通对象且不含 `signal` 字段,内核会按 shorthand 视为 `{ signal: payload, trigger: null }`(见 core 的 `routeSenseComputeOutput`)。
当前引擎:`compute(state)` 返回 `{ state, trigger }`,其中 `trigger``null``{ command: string }`(sense worker 内 `shell: true` 执行)。Workflow 仅通过 CLI / daemon IPC 启动,类型见 `@uncaged/nerve-core``SenseComputeFn` / `SenseTrigger`
### Sense 模块导出
+3 -30
View File
@@ -125,29 +125,6 @@ export async function compute(state) {
}
`;
/** First trigger launches local noop workflow; later triggers only advance idleTicks. */
const counterIndexJsWithNoopWorkflow = `export const initialState = { launched: false, idleTicks: 0 };
export async function compute(state) {
if (!state.launched) {
return {
state: { launched: true, idleTicks: state.idleTicks },
trigger: {
kind: "workflow",
name: "noop",
maxRounds: 3,
prompt: "e2e-archive",
dryRun: false,
},
};
}
return {
state: { launched: state.launched, idleTicks: state.idleTicks + 1 },
trigger: null,
};
}
`;
/** Minimal workflow: moderator ends immediately (no role rounds). */
const noopWorkflowIndexJs = `const END = "__end__";
export default {
@@ -205,11 +182,7 @@ function writeWorkspaceLayout(nerveRoot: string, withNoopWorkflow: boolean): voi
withNoopWorkflow ? nerveYamlWithNoopWorkflow : nerveYamlTemplate,
"utf8",
);
writeFileSync(
join(nerveRoot, "dist", "senses", "counter", "index.js"),
withNoopWorkflow ? counterIndexJsWithNoopWorkflow : counterIndexJs,
"utf8",
);
writeFileSync(join(nerveRoot, "dist", "senses", "counter", "index.js"), counterIndexJs, "utf8");
writeFileSync(
join(nerveRoot, "dist", "workflows", "echo", "index.js"),
echoWorkflowIndexJs,
@@ -235,8 +208,8 @@ export type TestDaemonHandle = {
export type StartTestDaemonOpts = {
/**
* When true, counter sense's first compute launches a local `noop` workflow (real
* workflow-worker child). Requires built `workflow-worker.js` next to `sense-worker.js`.
* When true, bundles a local `noop` workflow under `dist/workflows/noop` for tests that
* start runs via `nerve workflow trigger` (real workflow-worker child).
*/
withNoopWorkflow: boolean;
} | null;
@@ -46,8 +46,14 @@ describe("e2e store archive", () => {
daemon = await startTestDaemon({ withNoopWorkflow: true });
linkWorkspaceDaemonIntoNerveRoot(daemon.nerveRoot);
const triggerResult = await runCli(daemon, ["sense", "trigger", "counter"]);
expect(triggerResult.exitCode).toBe(0);
const wfTrigger = await runCli(daemon, [
"workflow",
"trigger",
"noop",
"--prompt",
"e2e-archive",
]);
expect(wfTrigger.exitCode).toBe(0);
const logsDb = join(daemon.nerveRoot, "data", "logs.db");
await pollUntil(() => {
@@ -101,8 +107,14 @@ describe("e2e store archive", () => {
daemon = await startTestDaemon({ withNoopWorkflow: true });
linkWorkspaceDaemonIntoNerveRoot(daemon.nerveRoot);
const triggerResult = await runCli(daemon, ["sense", "trigger", "counter"]);
expect(triggerResult.exitCode).toBe(0);
const wfTrigger = await runCli(daemon, [
"workflow",
"trigger",
"noop",
"--prompt",
"e2e-archive",
]);
expect(wfTrigger.exitCode).toBe(0);
const logsDb = join(daemon.nerveRoot, "data", "logs.db");
await pollUntil(() => {