1- import { mastra } from "@/src/mastra" ;
21import { loadRunBundle } from "@/db/client" ;
32import { profileById } from "@/db/client-profiles" ;
4- import { toTraceEvent , pipelineErrorEvent , type TraceEvent } from "@/lib/trace" ;
5- import { ndjsonLine } from "@/lib/ndjson" ;
6- import { checkRateLimit , clientIpFrom } from "@/lib/ratelimit" ;
73import {
84 RunRequest ,
95 STREAM_CONTENT_TYPE ,
106 type StreamDone ,
117} from "@/lib/api-types" ;
8+ import { ndjsonLine } from "@/lib/ndjson" ;
9+ import { checkRateLimit , clientIpFrom } from "@/lib/ratelimit" ;
10+ import { toTraceEvent , pipelineErrorEvent , type TraceEvent } from "@/lib/trace" ;
11+ import { mastra } from "@/src/mastra" ;
1212
1313/**
1414 * POST /api/run — execute the procure-to-pay pipeline for one seeded invoice and
@@ -37,11 +37,11 @@ export const runtime = "nodejs";
3737export const dynamic = "force-dynamic" ;
3838export const maxDuration = 60 ;
3939
40- function line ( obj : unknown ) : Uint8Array {
40+ const line = ( obj : unknown ) : Uint8Array => {
4141 return new TextEncoder ( ) . encode ( ndjsonLine ( obj ) ) ;
42- }
42+ } ;
4343
44- export async function POST ( request : Request ) : Promise < Response > {
44+ export const POST = async ( request : Request ) : Promise < Response > = > {
4545 // 0. Rate-limit by IP first — before any work or model calls. The demo is
4646 // public and each run spends Anthropic tokens, so this caps abuse. Fails
4747 // open if Redis isn't configured (see lib/ratelimit.ts).
@@ -64,27 +64,29 @@ export async function POST(request: Request): Promise<Response> {
6464
6565 // 1. Parse + validate the request body.
6666 let id : string ;
67- let decision : "approve" | "reject" | undefined ;
67+ let decisions : Record < string , "approve" | "reject" > ;
6868 let profileId : string | undefined ;
6969 try {
7070 const body : unknown = await request . json ( ) ;
7171 const parsed = RunRequest . parse ( body ) ;
7272 id = parsed . id ;
73- decision = parsed . decision ;
73+ decisions = parsed . decisions ?? { } ;
7474 profileId = parsed . profileId ;
7575 } catch {
7676 return Response . json (
7777 {
78- error : 'Body must be { id: string, decision?: "approve" | "reject" }.' ,
78+ error :
79+ 'Body must be { id: string, decisions?: { [stepId]: "approve" | "reject" } }.' ,
7980 } ,
8081 { status : 400 } ,
8182 ) ;
8283 }
83- // The reviewer's decision maps to the workflow's humanApproval input. No
84- // decision → "pending" (an exception pauses for a human). "approve"/"reject"
85- // are phase-2 resumes. Recomputing the cheap deterministic prefix instead of
86- // restoring a persisted snapshot is what keeps the human-in-the-loop stateless.
87- const humanApproval = decision ?? "pending" ;
84+ // The reviewer's per-step decisions drive the approval workflow. None → every
85+ // active gate pauses as pending (phase 1). A resume sends decisions by step id
86+ // (phase 2); the bill posts only once all active gates are approved. Recomputing
87+ // the deterministic prefix from the decisions (not a persisted snapshot) is what
88+ // keeps the human-in-the-loop stateless.
89+ const hasDecisions = Object . keys ( decisions ) . length > 0 ;
8890
8991 // 2. Load the seeded document bundle (READ ONLY). Done before opening the
9092 // stream so a missing invoice / missing DB config is a clean HTTP error
@@ -129,12 +131,12 @@ export async function POST(request: Request): Promise<Response> {
129131 purchaseOrder : bundle . purchaseOrder ,
130132 goodsReceipt : bundle . goodsReceipt ,
131133 priorInvoiceNumbers : bundle . priorInvoiceNumbers ,
132- humanApproval ,
133- // On a phase-2 resume (approve/reject ) the workflow re-runs from the
134+ decisions ,
135+ // On a phase-2 resume (decisions present ) the workflow re-runs from the
134136 // top, but the document was already read — skip the (costly) vision
135137 // call the second time. The reveal from phase 1 still stands.
136- skipExtraction : decision !== undefined ,
137- // Run under the requested client profile (tolerances + approval tiers );
138+ skipExtraction : hasDecisions ,
139+ // Run under the requested client profile (tolerances + approval workflow );
138140 // defaults to the standard profile.
139141 profile : profileById ( profileId ) ,
140142 } ,
@@ -176,4 +178,4 @@ export async function POST(request: Request): Promise<Response> {
176178 "x-accel-buffering" : "no" ,
177179 } ,
178180 } ) ;
179- }
181+ } ;
0 commit comments