feat(core,daemon): RFC-003 Phase 4 — WorkflowSpec Compiler
- WorkflowSpec + RoleSpec types in packages/core - compileWorkflowSpec: WorkflowSpec → WorkflowDefinition (daemon) - resolveRoleTimeoutMs: two-level timeout (role override > agent default) - parseDurationStringToMs extracted to shared duration.ts - AgentRegistry.getAgentConfig for timeout lookup - Tests: 10 new cases (compile shape, agent→extract flow, timeout resolution) - Backward compat: hand-written Role<Meta> unchanged Closes #238 Ref: #234
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveRoleTimeoutMs } from "../workflow-spec.js";
|
||||
|
||||
describe("resolveRoleTimeoutMs", () => {
|
||||
it("uses agent default when role timeout is null", () => {
|
||||
const r = resolveRoleTimeoutMs(null, 300_000);
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) expect(r.value).toBe(300_000);
|
||||
});
|
||||
|
||||
it("uses role override string over agent default", () => {
|
||||
const r = resolveRoleTimeoutMs("60s", 300_000);
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) expect(r.value).toBe(60_000);
|
||||
});
|
||||
|
||||
it("allows explicit role duration when agent default is null", () => {
|
||||
const r = resolveRoleTimeoutMs("5s", null);
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) expect(r.value).toBe(5000);
|
||||
});
|
||||
|
||||
it("returns err for invalid duration string", () => {
|
||||
const r = resolveRoleTimeoutMs("not-a-duration", 300_000);
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Result } from "./result.js";
|
||||
import { err, ok } from "./result.js";
|
||||
|
||||
const DURATION_RE = /^(\d+)([smh])$/;
|
||||
|
||||
const DURATION_MULTIPLIERS: Record<string, number> = {
|
||||
s: 1_000,
|
||||
m: 60_000,
|
||||
h: 3_600_000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a duration string such as `5s`, `10m`, `1h` to milliseconds.
|
||||
* Used by `parseNerveConfig` and WorkflowSpec role timeout (RFC-003).
|
||||
*/
|
||||
export function parseDurationStringToMs(value: string): Result<number> {
|
||||
const match = DURATION_RE.exec(value);
|
||||
if (!match) {
|
||||
return err(new Error(`invalid duration "${value}" (expected e.g. "5s", "10m", "1h")`));
|
||||
}
|
||||
return ok(Number(match[1]) * DURATION_MULTIPLIERS[match[2]]);
|
||||
}
|
||||
@@ -27,6 +27,9 @@ export type {
|
||||
WorkflowDefinition,
|
||||
} from "./workflow.js";
|
||||
export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "./workflow.js";
|
||||
export type { RoleSpec, WorkflowSpec } from "./workflow-spec.js";
|
||||
export { resolveRoleTimeoutMs } from "./workflow-spec.js";
|
||||
export { parseDurationStringToMs } from "./duration.js";
|
||||
export type { Schema, ExtractFn } from "./extract-layer.js";
|
||||
export { ExtractError } from "./extract-layer.js";
|
||||
export type { Result } from "./result.js";
|
||||
|
||||
@@ -9,25 +9,12 @@ import {
|
||||
type SenseConfig,
|
||||
type WorkflowConfig,
|
||||
} from "./config.js";
|
||||
import { parseDurationStringToMs } from "./duration.js";
|
||||
import { isPlainRecord } from "./is-plain-record.js";
|
||||
import type { Result } from "./result.js";
|
||||
import { err, ok } from "./result.js";
|
||||
import { DEFAULT_ENGINE_MAX_ROUNDS } from "./workflow.js";
|
||||
|
||||
const DURATION_RE = /^(\d+)([smh])$/;
|
||||
|
||||
const DURATION_MULTIPLIERS: Record<string, number> = {
|
||||
s: 1_000,
|
||||
m: 60_000,
|
||||
h: 3_600_000,
|
||||
};
|
||||
|
||||
function parseDurationToMs(value: string): number | null {
|
||||
const match = DURATION_RE.exec(value);
|
||||
if (!match) return null;
|
||||
return Number(match[1]) * DURATION_MULTIPLIERS[match[2]];
|
||||
}
|
||||
|
||||
function isValidGroupName(value: string): boolean {
|
||||
return /^[a-zA-Z0-9_-]+$/.test(value);
|
||||
}
|
||||
@@ -54,13 +41,11 @@ function parseDurationField(field: unknown, label: string): Result<number | null
|
||||
new Error(`${label}: invalid duration "${field}" (expected e.g. "5s", "10m", "1h")`),
|
||||
);
|
||||
}
|
||||
const ms = parseDurationToMs(field);
|
||||
if (ms === null) {
|
||||
return err(
|
||||
new Error(`${label}: invalid duration "${field}" (expected e.g. "5s", "10m", "1h")`),
|
||||
);
|
||||
const msResult = parseDurationStringToMs(field);
|
||||
if (!msResult.ok) {
|
||||
return err(new Error(`${label}: ${msResult.error.message}`));
|
||||
}
|
||||
return ok(ms);
|
||||
return ok(msResult.value);
|
||||
}
|
||||
|
||||
function validateSenseConfig(name: string, raw: unknown): Result<SenseConfig> {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { parseDurationStringToMs } from "./duration.js";
|
||||
import type { Schema } from "./extract-layer.js";
|
||||
import type { Result } from "./result.js";
|
||||
import { ok } from "./result.js";
|
||||
import type { Moderator, RoleMeta } from "./workflow.js";
|
||||
|
||||
/**
|
||||
* Authoring-time role: references a named agent, prompt, extract schema, and optional timeout.
|
||||
* Compiles to runtime `Role<Meta>` via `compileWorkflowSpec` (RFC-003 Phase 4).
|
||||
*/
|
||||
export type RoleSpec<Meta extends Record<string, unknown>> = {
|
||||
agent: string;
|
||||
prompt: string;
|
||||
meta: Schema<Meta>;
|
||||
/** Override agent default; `null` uses the agent's configured timeout from `nerve.yaml`. */
|
||||
timeout: string | null;
|
||||
};
|
||||
|
||||
/** User-facing workflow authoring shape; compiles to `WorkflowDefinition`. */
|
||||
export type WorkflowSpec<M extends RoleMeta> = {
|
||||
name: string;
|
||||
roles: { [K in keyof M]: RoleSpec<M[K]> };
|
||||
moderator: Moderator<M>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Two-level timeout: explicit role string wins; otherwise agent default (milliseconds).
|
||||
*/
|
||||
export function resolveRoleTimeoutMs(
|
||||
roleTimeout: string | null,
|
||||
agentDefaultMs: number | null,
|
||||
): Result<number | null> {
|
||||
if (roleTimeout === null) {
|
||||
return ok(agentDefaultMs);
|
||||
}
|
||||
return parseDurationStringToMs(roleTimeout);
|
||||
}
|
||||
Reference in New Issue
Block a user