refactor(core): restructure ModeratorContext to { start, steps }

- ModeratorContext: discriminated union → { start: StartStep; steps: RoleStep<M>[] }
- Moderator signature: (context, round, maxRounds) → (context)
- round derivable from steps.length, maxRounds from start.meta.maxRounds
- workflow-worker.ts: build steps array, pass full context to moderator
- Remove unused ModeratorContext import from workflow-worker
- Update README.md

Refs #110
This commit is contained in:
2026-04-25 02:48:17 +00:00
parent 0fff8ef954
commit 3ce9e3a846
3 changed files with 38 additions and 31 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ Shared types and configuration parser for the [nerve](../../README.md) observati
- **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`
- **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`, `Moderator`, `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`, `SenseResult`, plus `DEFAULT_ENGINE_MAX_ROUNDS`
- **Result type** — `Result<T>` with `ok()` / `err()` helpers for explicit error handling (no thrown exceptions for parse paths)
## Usage
+10 -9
View File
@@ -103,21 +103,22 @@ export type RoleStep<M extends RoleMeta> = {
}[keyof M & string];
/**
* Moderator input: either the initial start frame or a role step after a step.
* Lets implementations branch on `context.kind` with full typing for each arm.
* Moderator input: the complete workflow history.
* Contains the start frame and all role steps so far.
* On initial call, `steps` is empty — moderator can check `steps.length === 0`.
* Round count is `steps.length`; maxRounds is in `start.meta.maxRounds`.
*/
export type ModeratorContext<M extends RoleMeta> =
| { kind: "start"; start: StartStep }
| { kind: "step"; step: RoleStep<M> };
export type ModeratorContext<M extends RoleMeta> = {
start: StartStep;
steps: RoleStep<M>[];
};
/**
* The moderator — a pure routing function. Receives start vs step context,
* current round, and maxRounds. Returns the next role name or END.
* The moderator — a pure routing function. Receives the full workflow context
* (start frame + all prior steps). Returns the next role name or END.
*/
export type Moderator<M extends RoleMeta> = (
context: ModeratorContext<M>,
round: number,
maxRounds: number,
) => (keyof M & string) | END;
/** The complete definition of a workflow, as authored by users. */
+27 -21
View File
@@ -12,13 +12,7 @@
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import type {
ModeratorContext,
RoleMeta,
StartStep,
WorkflowDefinition,
WorkflowMessage,
} from "@uncaged/nerve-core";
import type { RoleMeta, StartStep, WorkflowDefinition, WorkflowMessage } from "@uncaged/nerve-core";
import { END, START, isPlainRecord } from "@uncaged/nerve-core";
import type {
@@ -208,15 +202,31 @@ async function runThread(
dryRun,
);
let roleRound = roleMessages.length;
let nextRole = def.moderator({ kind: "start", start }, roleRound, maxRounds);
const steps: Array<{
role: string;
meta: Record<string, unknown>;
content: string;
timestamp: number;
}> = [];
// Rebuild steps from any resumed messages
for (const msg of roleMessages) {
steps.push({
role: msg.role,
meta: msg.meta as Record<string, unknown>,
content: msg.content,
timestamp: msg.timestamp,
});
}
let nextRole = def.moderator({ start, steps });
if (nextRole === END) {
sendThreadEvent(runId, "completed", null);
return;
}
while (roleRound < maxRounds) {
while (steps.length < maxRounds) {
const result = await executeRole(def, nextRole, start, roleMessages, runId);
if (result === null) return;
@@ -229,18 +239,14 @@ async function runThread(
roleMessages.push(message);
sendWorkflowMessage(runId, message);
roleRound += 1;
steps.push({
role: nextRole,
meta: result.meta,
content: result.content,
timestamp: message.timestamp,
});
const stepContext: ModeratorContext<RoleMeta> = {
kind: "step",
step: {
role: nextRole,
meta: result.meta,
content: result.content,
timestamp: message.timestamp,
},
};
nextRole = def.moderator(stepContext, roleRound, maxRounds);
nextRole = def.moderator({ start, steps });
if (nextRole === END) {
sendThreadEvent(runId, "completed", null);