/** * Human-in-the-loop review protocol. * * Coordinates review requests, decisions, escalation chains, * and partial approval workflows for durable human oversight. * * @module human-review */ import { z } from 'zod'; import type { BatchOperation, Storage } from '../../storage/interface.ts'; import { WeftError } from '../weft-error.ts'; /** * A persisted human review request. Created by {@link ReviewCoordinator.createReview} * and returned to the workflow once a reviewer submits a decision. * * @example * ```ts * import type { ReviewRequest } from '@lostgradient/weft'; * * const request: ReviewRequest = { * reviewId: 'r-1', * workflowId: 'wf-1', * artifact: { text: 'draft' }, * reviewType: 'content', * reviewers: ['alice@example.com'], * allowPartial: false, * createdAt: Date.now(), * }; * ``` */ export interface ReviewRequest { reviewId: string; workflowId: string; artifact: unknown; reviewType: string; reviewers: string[]; allowPartial: boolean; timeout?: number; webhookUrl?: string; createdAt: number; } /** * A reviewer's decision payload returned to the workflow from `ctx.review()`. * The `decision` field is the binary outcome; `sectionDecisions` carries * per-section verdicts when `allowPartial` is enabled on the request. */ export interface ReviewDecisionRecord { reviewId: string; decision: 'approved' | 'rejected' | 'needs-changes'; reviewer: string; feedback?: string; sectionDecisions?: Record; timestamp: number; } /** Canonical schema for a persisted review request without its storage envelope. */ export declare const reviewRequestSchema: z.ZodObject<{ reviewId: z.ZodString; workflowId: z.ZodString; artifact: z.ZodNonOptional; reviewType: z.ZodString; reviewers: z.ZodArray; allowPartial: z.ZodBoolean; timeout: z.ZodOptional; webhookUrl: z.ZodOptional; createdAt: z.ZodNumber; }, z.core.$strip>; /** Canonical schema for a pending review entry returned by review list surfaces. */ export declare const pendingReviewEntrySchema: z.ZodObject<{ reviewId: z.ZodString; workflowId: z.ZodString; artifact: z.ZodNonOptional; reviewType: z.ZodString; reviewers: z.ZodArray; allowPartial: z.ZodBoolean; timeout: z.ZodOptional; webhookUrl: z.ZodOptional; createdAt: z.ZodNumber; status: z.ZodLiteral<"pending">; }, z.core.$strip>; /** Canonical schema for a completed review entry returned by review list surfaces. */ export declare const completedReviewEntrySchema: z.ZodObject<{ decision: z.ZodEnum<{ approved: "approved"; rejected: "rejected"; "needs-changes": "needs-changes"; }>; reviewer: z.ZodString; feedback: z.ZodOptional; sectionDecisions: z.ZodOptional>>; timestamp: z.ZodNumber; reviewId: z.ZodString; workflowId: z.ZodString; artifact: z.ZodNonOptional; reviewType: z.ZodString; reviewers: z.ZodArray; allowPartial: z.ZodBoolean; timeout: z.ZodOptional; webhookUrl: z.ZodOptional; createdAt: z.ZodNumber; status: z.ZodLiteral<"completed">; }, z.core.$strip>; /** Canonical schema for the discriminated review list-entry union. */ export declare const reviewListEntrySchema: z.ZodUnion; reviewType: z.ZodString; reviewers: z.ZodArray; allowPartial: z.ZodBoolean; timeout: z.ZodOptional; webhookUrl: z.ZodOptional; createdAt: z.ZodNumber; status: z.ZodLiteral<"pending">; }, z.core.$strip>, z.ZodObject<{ decision: z.ZodEnum<{ approved: "approved"; rejected: "rejected"; "needs-changes": "needs-changes"; }>; reviewer: z.ZodString; feedback: z.ZodOptional; sectionDecisions: z.ZodOptional>>; timestamp: z.ZodNumber; reviewId: z.ZodString; workflowId: z.ZodString; artifact: z.ZodNonOptional; reviewType: z.ZodString; reviewers: z.ZodArray; allowPartial: z.ZodBoolean; timeout: z.ZodOptional; webhookUrl: z.ZodOptional; createdAt: z.ZodNumber; status: z.ZodLiteral<"completed">; }, z.core.$strip>]>; /** * One step in a {@link ReviewOptions.escalation} chain. Either reassigns the * pending review to a new owner (`to`) or auto-decides it (`action`) after * `after` milliseconds have elapsed. * * @example * ```ts * import type { EscalationStep } from '@lostgradient/weft'; * * const step: EscalationStep = { after: 60_000, to: 'manager@example.com' }; * void step; * ``` */ export interface EscalationStep { after: number; to?: string; action?: 'auto-approve' | 'auto-reject'; auditReason?: string; } /** * Base options for creating a review via {@link ReviewCoordinator.createReview}. * The `artifact` carries the payload the reviewer evaluates; the other fields * configure routing, partial approval, escalation chains, and webhook delivery. * * @example * ```ts * import type { ReviewOptions } from '@lostgradient/weft'; * * const options: ReviewOptions = { * artifact: { summary: 'release plan' }, * reviewers: ['alice@example.com'], * timeout: 60_000, * }; * void options; * ``` */ export interface ReviewOptions { artifact: unknown; reviewType?: string; reviewers?: string[]; allowPartial?: boolean; timeout?: number; escalation?: EscalationStep[]; webhookUrl?: string; } /** * Options passed to `ctx.review()` inside a workflow generator. Extends * {@link ReviewOptions} with the context-level callback the runtime invokes * when an escalation step fires. * * @example * ```ts * import type { HumanReviewOptions } from '@lostgradient/weft'; * * const options: HumanReviewOptions = { * artifact: 'release plan', * reviewers: ['alice@example.com'], * onEscalation: (action) => console.log('escalation:', action), * }; * void options; * ``` */ export interface HumanReviewOptions extends ReviewOptions { /** Handler called when an escalation step fires. */ onEscalation?: (action: EscalationAction) => void; } /** * The decision payload returned to the workflow from `ctx.review()`. * Alias for {@link ReviewDecisionRecord}. * * @example * ```ts * import type { HumanReviewResult } from '@lostgradient/weft'; * * const result: HumanReviewResult = { * reviewId: 'r-1', * decision: 'approved', * reviewer: 'alice@example.com', * timestamp: Date.now(), * }; * void result; * ``` */ export type HumanReviewResult = ReviewDecisionRecord; /** * The action returned by {@link ReviewCoordinator.checkEscalations} when an * escalation step has fired. Either reassigns the review to a new owner or * auto-decides it with an audit reason. * * @example * ```ts * import type { EscalationAction } from '@lostgradient/weft'; * * const action: EscalationAction = { type: 'escalate', to: 'manager@example.com' }; * void action; * ``` */ export type EscalationAction = { type: 'escalate'; to: string; } | { type: 'auto-decide'; decision: 'approved' | 'rejected'; auditReason: string; }; /** * Thrown when a human review request exceeds its configured `timeout` * milliseconds without receiving a decision. Carries the `reviewId` and the * elapsed time so callers can decide whether to escalate or auto-approve. * * @example Catch a review timeout and escalate * ```ts * import { workflow, ReviewTimeoutError } from '@lostgradient/weft'; * import type { Context, WorkflowContext } from '@lostgradient/weft'; * import { TestEngine } from '@lostgradient/weft/testing'; * * const engine = new TestEngine({ startTime: 0 }); * engine.register( * workflow({ name: 'needs-review' }).execute(async function* (ctx: WorkflowContext) { * return yield* ctx.review({ * artifact: 'release plan', * reviewers: ['alice@example.com'], * timeout: 5_000, * }); * }), * ); * const handle = await engine.start('needs-review', null); * const result = handle.result(); * await engine.advanceTime(6_000); * * try { * await result; * } catch (error) { * if (error instanceof ReviewTimeoutError) { * console.warn(`Review ${error.reviewId} timed out after ${error.elapsed}ms`); * } * } * ``` */ export declare class ReviewTimeoutError extends WeftError<'ReviewTimeoutError'> { readonly reviewId: string; readonly elapsed: number; constructor(reviewId: string, elapsed: number); } /** * Options for constructing a {@link ReviewCoordinator}. Accepts an optional * `EventTarget` to dispatch {@link ReviewRequestedEvent} on review * creation, and a custom `getNow` clock function for deterministic testing. * * @example Attach an event target and a fixed clock for tests * ```ts * import { ReviewCoordinator, type ReviewCoordinatorOptions } from '@lostgradient/weft'; * import { MemoryStorage } from '@lostgradient/weft/storage/memory'; * * const storage = new MemoryStorage(); * const options: ReviewCoordinatorOptions = { * eventTarget: new EventTarget(), * getNow: () => 1_700_000_000_000, * }; * * const coordinator = new ReviewCoordinator(storage, options); * ``` */ export interface ReviewCoordinatorOptions { /** When provided, the coordinator dispatches human review events. */ eventTarget?: EventTarget; /** Custom time source for testing. Defaults to `Date.now`. */ getNow?: () => number; } /** * Persists human review requests to storage, dispatches * {@link ReviewRequestedEvent} on creation, accepts reviewer decisions, * and checks escalation timeouts. Used by `ctx.review()` inside workflow * generators to pause execution pending a human decision. * * @example Create a review and later submit a decision * ```ts * import { ReviewCoordinator } from '@lostgradient/weft'; * import { MemoryStorage } from '@lostgradient/weft/storage/memory'; * * const storage = new MemoryStorage(); * const coordinator = new ReviewCoordinator(storage); * * const review = await coordinator.createReview('wf-123', { * artifact: { text: 'Draft email body…' }, * reviewType: 'content', * reviewers: ['alice@example.com'], * }); * * const decision = await coordinator.submitDecision(review.reviewId, { * decision: 'approved', * reviewer: 'alice@example.com', * }); * console.log(decision.decision); // 'approved' * ``` */ export declare class ReviewCoordinator { #private; constructor(storage: Storage, optionsOrGetNow?: ReviewCoordinatorOptions | (() => number)); /** Create a review request and persist it. */ createReview(workflowId: string, options: ReviewOptions): Promise; /** Submit a review decision. */ submitDecision(reviewId: string, decision: Omit): Promise; /** Get a pending review. */ getReview(workflowId: string, reviewId: string): Promise; /** List pending reviews. */ listPendingReviews(): Promise; /** Build cleanup operations for completed workflow. */ cleanupOperations(workflowId: string, reviewId: string): BatchOperation[]; /** Check for escalation timeouts. Returns actions to take. */ checkEscalations(review: ReviewRequest, escalation: EscalationStep[], now: number): EscalationAction | null; }