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:
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user