Merge pull request 'refactor(workflow): move descriptor validation out of runtime' (#135) from refactor/runtime-descriptor-boundary into main
This commit is contained in:
@@ -1,2 +0,0 @@
|
|||||||
export type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js";
|
|
||||||
export { validateWorkflowDescriptor } from "./workflow-descriptor.js";
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */
|
|
||||||
export type WorkflowRoleSchema = Record<string, unknown>;
|
|
||||||
|
|
||||||
export type WorkflowRoleDescriptor = {
|
|
||||||
description: string;
|
|
||||||
schema: WorkflowRoleSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */
|
|
||||||
export type WorkflowDescriptor = {
|
|
||||||
description: string;
|
|
||||||
roles: Record<string, WorkflowRoleDescriptor>;
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { err, ok, type Result } from "../util/index.js";
|
|
||||||
|
|
||||||
import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js";
|
|
||||||
|
|
||||||
export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> {
|
|
||||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
||||||
return err("descriptor must be a non-array object");
|
|
||||||
}
|
|
||||||
const root = value as Record<string, unknown>;
|
|
||||||
const description = root.description;
|
|
||||||
if (typeof description !== "string") {
|
|
||||||
return err("descriptor.description must be a string");
|
|
||||||
}
|
|
||||||
const rolesRaw = root.roles;
|
|
||||||
if (rolesRaw === null || typeof rolesRaw !== "object" || Array.isArray(rolesRaw)) {
|
|
||||||
return err("descriptor.roles must be a non-array object");
|
|
||||||
}
|
|
||||||
|
|
||||||
const roles: Record<string, WorkflowRoleDescriptor> = {};
|
|
||||||
for (const [roleName, specUnknown] of Object.entries(rolesRaw)) {
|
|
||||||
if (specUnknown === null || typeof specUnknown !== "object" || Array.isArray(specUnknown)) {
|
|
||||||
return err(`descriptor.roles.${roleName} must be a non-array object`);
|
|
||||||
}
|
|
||||||
const spec = specUnknown as Record<string, unknown>;
|
|
||||||
const roleDesc = spec.description;
|
|
||||||
if (typeof roleDesc !== "string") {
|
|
||||||
return err(`descriptor.roles.${roleName}.description must be a string`);
|
|
||||||
}
|
|
||||||
const schema = spec.schema;
|
|
||||||
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
||||||
return err(`descriptor.roles.${roleName}.schema must be a non-array object`);
|
|
||||||
}
|
|
||||||
roles[roleName] = {
|
|
||||||
description: roleDesc,
|
|
||||||
schema: schema as WorkflowRoleSchema,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ description, roles });
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export type { CasStore } from "./types.js";
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export type CasStore = {
|
|
||||||
put(content: string): Promise<string>;
|
|
||||||
get(hash: string): Promise<string | null>;
|
|
||||||
delete(hash: string): Promise<void>;
|
|
||||||
list(): Promise<string[]>;
|
|
||||||
};
|
|
||||||
+6
-6
@@ -1,10 +1,10 @@
|
|||||||
import type * as z from "zod/v4";
|
import type * as z from "zod/v4";
|
||||||
|
|
||||||
import type { CasStore } from "../cas/types.js";
|
|
||||||
import {
|
import {
|
||||||
type AgentBinding,
|
type AgentBinding,
|
||||||
type AgentContext,
|
type AgentContext,
|
||||||
type AgentFn,
|
type AgentFn,
|
||||||
|
type CasStore,
|
||||||
END,
|
END,
|
||||||
type ExtractContext,
|
type ExtractContext,
|
||||||
type ModeratorContext,
|
type ModeratorContext,
|
||||||
@@ -18,8 +18,7 @@ import {
|
|||||||
type WorkflowDefinition,
|
type WorkflowDefinition,
|
||||||
type WorkflowFn,
|
type WorkflowFn,
|
||||||
type WorkflowRuntime,
|
type WorkflowRuntime,
|
||||||
} from "../types.js";
|
} from "./types.js";
|
||||||
import { mergeRefsWithContentHash } from "../util/index.js";
|
|
||||||
|
|
||||||
function isRoleNext<M extends RoleMeta>(
|
function isRoleNext<M extends RoleMeta>(
|
||||||
next: (keyof M & string) | typeof END,
|
next: (keyof M & string) | typeof END,
|
||||||
@@ -97,10 +96,11 @@ async function advanceOneRound<M extends RoleMeta>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const contentHash = await putContentBlob(runtime.cas, raw);
|
const contentHash = await putContentBlob(runtime.cas, raw);
|
||||||
const refs = mergeRefsWithContentHash(
|
const refsFromMeta = resolveExtractedRefs(
|
||||||
resolveExtractedRefs(roleDef as unknown as RoleDefinition<Record<string, unknown>>, meta),
|
roleDef as unknown as RoleDefinition<Record<string, unknown>>,
|
||||||
contentHash,
|
meta,
|
||||||
);
|
);
|
||||||
|
const refs = refsFromMeta.includes(contentHash) ? refsFromMeta : [...refsFromMeta, contentHash];
|
||||||
|
|
||||||
const step = {
|
const step = {
|
||||||
role: next,
|
role: next,
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { createWorkflow } from "./create-workflow.js";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export type { ExtractFn } from "./types.js";
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type * as z from "zod/v4";
|
|
||||||
|
|
||||||
import type { ExtractContext } from "../types.js";
|
|
||||||
|
|
||||||
export type ExtractFn = <T extends Record<string, unknown>>(
|
|
||||||
schema: z.ZodType<T>,
|
|
||||||
prompt: string,
|
|
||||||
ctx: ExtractContext,
|
|
||||||
) => Promise<T>;
|
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
export type {
|
export { createWorkflow } from "./create-workflow.js";
|
||||||
WorkflowDescriptor,
|
export { err, ok } from "./result.js";
|
||||||
WorkflowRoleDescriptor,
|
|
||||||
WorkflowRoleSchema,
|
|
||||||
} from "./bundle/types.js";
|
|
||||||
export { validateWorkflowDescriptor } from "./bundle/workflow-descriptor.js";
|
|
||||||
export type { CasStore } from "./cas/index.js";
|
|
||||||
export { createWorkflow } from "./engine/index.js";
|
|
||||||
export type { ExtractFn } from "./extract/index.js";
|
|
||||||
export type {
|
export type {
|
||||||
AgentBinding,
|
AgentBinding,
|
||||||
AgentContext,
|
AgentContext,
|
||||||
AgentFn,
|
AgentFn,
|
||||||
|
CasStore,
|
||||||
ExtractContext,
|
ExtractContext,
|
||||||
|
ExtractFn,
|
||||||
LlmProvider,
|
LlmProvider,
|
||||||
Moderator,
|
Moderator,
|
||||||
ModeratorContext,
|
ModeratorContext,
|
||||||
|
Result,
|
||||||
RoleDefinition,
|
RoleDefinition,
|
||||||
RoleMeta,
|
RoleMeta,
|
||||||
RoleOutput,
|
RoleOutput,
|
||||||
@@ -23,10 +19,11 @@ export type {
|
|||||||
ThreadContext,
|
ThreadContext,
|
||||||
WorkflowCompletion,
|
WorkflowCompletion,
|
||||||
WorkflowDefinition,
|
WorkflowDefinition,
|
||||||
|
WorkflowDescriptor,
|
||||||
WorkflowFn,
|
WorkflowFn,
|
||||||
WorkflowResult,
|
WorkflowResult,
|
||||||
|
WorkflowRoleDescriptor,
|
||||||
|
WorkflowRoleSchema,
|
||||||
WorkflowRuntime,
|
WorkflowRuntime,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
export { END, START } from "./types.js";
|
export { END, START } from "./types.js";
|
||||||
export type { Result } from "./util/index.js";
|
|
||||||
export { err, ok } from "./util/index.js";
|
|
||||||
|
|||||||
@@ -1,12 +1,33 @@
|
|||||||
import type * as z from "zod/v4";
|
import type * as z from "zod/v4";
|
||||||
|
|
||||||
import type { CasStore } from "./cas/index.js";
|
|
||||||
import type { ExtractFn } from "./extract/types.js";
|
|
||||||
|
|
||||||
/** Sentinel values for automaton control flow. */
|
/** Sentinel values for automaton control flow. */
|
||||||
export const START = "__start__" as const;
|
export const START = "__start__" as const;
|
||||||
export const END = "__end__" as const;
|
export const END = "__end__" as const;
|
||||||
|
|
||||||
|
export type CasStore = {
|
||||||
|
put(content: string): Promise<string>;
|
||||||
|
get(hash: string): Promise<string | null>;
|
||||||
|
delete(hash: string): Promise<void>;
|
||||||
|
list(): Promise<string[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */
|
||||||
|
export type WorkflowRoleSchema = Record<string, unknown>;
|
||||||
|
|
||||||
|
export type WorkflowRoleDescriptor = {
|
||||||
|
description: string;
|
||||||
|
schema: WorkflowRoleSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */
|
||||||
|
export type WorkflowDescriptor = {
|
||||||
|
description: string;
|
||||||
|
roles: Record<string, WorkflowRoleDescriptor>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Expected success/failure outcome without throwing for recoverable errors. */
|
||||||
|
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
||||||
|
|
||||||
/** Maps role names → their meta types. Single generic drives all inference. */
|
/** Maps role names → their meta types. Single generic drives all inference. */
|
||||||
export type RoleMeta = Record<string, Record<string, unknown>>;
|
export type RoleMeta = Record<string, Record<string, unknown>>;
|
||||||
|
|
||||||
@@ -96,6 +117,12 @@ export type ExtractContext<M extends RoleMeta = RoleMeta> = AgentContext<M> & {
|
|||||||
agentContent: string;
|
agentContent: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExtractFn = <T extends Record<string, unknown>>(
|
||||||
|
schema: z.ZodType<T>,
|
||||||
|
prompt: string,
|
||||||
|
ctx: ExtractContext,
|
||||||
|
) => Promise<T>;
|
||||||
|
|
||||||
/** Raw string output from an LLM/CLI adapter; meta is extracted by the engine. */
|
/** Raw string output from an LLM/CLI adapter; meta is extracted by the engine. */
|
||||||
export type AgentFn = (ctx: AgentContext) => Promise<string>;
|
export type AgentFn = (ctx: AgentContext) => Promise<string>;
|
||||||
|
|
||||||
@@ -131,3 +158,8 @@ export type WorkflowDefinition<M extends RoleMeta> = {
|
|||||||
roles: { [K in keyof M & string]: RoleDefinition<M[K]> };
|
roles: { [K in keyof M & string]: RoleDefinition<M[K]> };
|
||||||
moderator: Moderator<M>;
|
moderator: Moderator<M>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Internal outcome of advancing one moderator round inside {@link createWorkflow}. */
|
||||||
|
export type AdvanceOutcome<M extends RoleMeta> =
|
||||||
|
| { kind: "complete"; completion: WorkflowCompletion }
|
||||||
|
| { kind: "yield"; output: RoleOutput; step: RoleStep<M> };
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export { mergeRefsWithContentHash } from "./refs-field.js";
|
|
||||||
export { err, ok } from "./result.js";
|
|
||||||
export type { Result } from "./types.js";
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/** Append `contentHash` to `refs` when not already present (dedupe by first occurrence order). */
|
|
||||||
export function mergeRefsWithContentHash(refs: string[], contentHash: string): string[] {
|
|
||||||
const out = [...refs];
|
|
||||||
if (!out.includes(contentHash)) {
|
|
||||||
out.push(contentHash);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
|
||||||
@@ -1 +1,40 @@
|
|||||||
export { validateWorkflowDescriptor } from "@uncaged/workflow-runtime";
|
import { err, ok, type Result } from "../util/index.js";
|
||||||
|
|
||||||
|
import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js";
|
||||||
|
|
||||||
|
export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> {
|
||||||
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return err("descriptor must be a non-array object");
|
||||||
|
}
|
||||||
|
const root = value as Record<string, unknown>;
|
||||||
|
const description = root.description;
|
||||||
|
if (typeof description !== "string") {
|
||||||
|
return err("descriptor.description must be a string");
|
||||||
|
}
|
||||||
|
const rolesRaw = root.roles;
|
||||||
|
if (rolesRaw === null || typeof rolesRaw !== "object" || Array.isArray(rolesRaw)) {
|
||||||
|
return err("descriptor.roles must be a non-array object");
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles: Record<string, WorkflowRoleDescriptor> = {};
|
||||||
|
for (const [roleName, specUnknown] of Object.entries(rolesRaw)) {
|
||||||
|
if (specUnknown === null || typeof specUnknown !== "object" || Array.isArray(specUnknown)) {
|
||||||
|
return err(`descriptor.roles.${roleName} must be a non-array object`);
|
||||||
|
}
|
||||||
|
const spec = specUnknown as Record<string, unknown>;
|
||||||
|
const roleDesc = spec.description;
|
||||||
|
if (typeof roleDesc !== "string") {
|
||||||
|
return err(`descriptor.roles.${roleName}.description must be a string`);
|
||||||
|
}
|
||||||
|
const schema = spec.schema;
|
||||||
|
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
||||||
|
return err(`descriptor.roles.${roleName}.schema must be a non-array object`);
|
||||||
|
}
|
||||||
|
roles[roleName] = {
|
||||||
|
description: roleDesc,
|
||||||
|
schema: schema as WorkflowRoleSchema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({ description, roles });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user