@@ -73,44 +73,38 @@ const EXTRACTION_TIMEOUT_MS = 20_000;
7373 notes it and proceeds on the record.
7474
7575 The reveal needs the document on screen the instant the run starts (before the
76- model returns), so we write an `intake-document` chunk early via the stream
77- writer; the step's final output then carries `{ extracted, matches }`. */
78- const IntakeOut = RunInput . merge ( Narrated ) . merge (
79- z . object ( {
80- extracted : Invoice . nullable ( ) ,
81- matches : z . boolean ( ) ,
82- } ) ,
83- ) ;
76+ model returns), and the final extraction result, are written to the TRACE via
77+ the stream writer (an `intake-document` then an `intake-result` chunk) — NOT
78+ threaded into the step output, because the downstream steps don't consume them
79+ (they run on the trusted record). So intake's output is the run input passed
80+ through unchanged: matching receives exactly what it uses, no phantom fields. */
8481const intakeStep = createStep ( {
8582 id : "intake" ,
8683 inputSchema : RunInput ,
87- outputSchema : IntakeOut ,
84+ outputSchema : RunInput ,
8885 execute : async ( { inputData, writer } ) => {
8986 const seed = inputData . invoice ;
9087
9188 // Phase-2 resume: the document was already read in phase 1, so skip the vision
9289 // call and pass straight through (the reveal from phase 1 still stands).
93- if ( inputData . skipExtraction ) {
94- return { ...inputData , extracted : null , matches : false , narration : "" } ;
95- }
90+ if ( inputData . skipExtraction ) return inputData ;
91+
92+ const emit = async ( chunk : unknown ) => {
93+ try {
94+ await writer ?. write ( chunk ) ;
95+ } catch {
96+ /* ignore writer errors — never let the trace affect the result */
97+ }
98+ } ;
9699
97100 // Show the document immediately (the on-screen twin of the PDF being read).
98- try {
99- await writer ?. write ( {
100- type : "intake-document" ,
101- payload : { document : seed } ,
102- } ) ;
103- } catch {
104- /* ignore writer errors — never let the trace affect the result */
105- }
101+ await emit ( { type : "intake-document" , payload : { document : seed } } ) ;
106102
107- let extracted : Invoice | null = null ;
108- let matches = false ;
109- let narration =
110- "Couldn't extract the document just now; proceeding on the seeded record." ;
103+ let result : { extracted : Invoice ; matches : boolean } | { extracted : null } =
104+ { extracted : null } ;
111105 try {
112106 const pdfBase64 = await renderInvoicePdfBase64 ( seed ) ;
113- const result = await Promise . race ( [
107+ const out = await Promise . race ( [
114108 extractInvoice ( pdfBase64 ) ,
115109 new Promise < { ok : false ; kind : "timeout" } > ( ( resolve ) =>
116110 setTimeout (
@@ -119,25 +113,22 @@ const intakeStep = createStep({
119113 ) ,
120114 ) ,
121115 ] ) ;
122- if ( result . ok ) {
123- extracted = result . invoice ;
116+ if ( out . ok ) {
124117 // Did the model's read agree with the seeded record on the fields the
125118 // pipeline routes on? (Drives the "reconciled with PO record" note.)
126- matches =
127- result . invoice . invoiceNumber === seed . invoiceNumber &&
128- ( result . invoice . poNumber ?? null ) === ( seed . poNumber ?? null ) &&
129- Math . abs ( result . invoice . total - seed . total ) < 0.01 ;
130- narration = matches
131- ? "Extracted and reconciled with the PO record."
132- : "Extracted; key fields differ from the record (pipeline uses the record)." ;
119+ const matches =
120+ out . invoice . invoiceNumber === seed . invoiceNumber &&
121+ ( out . invoice . poNumber ?? null ) === ( seed . poNumber ?? null ) &&
122+ Math . abs ( out . invoice . total - seed . total ) < 0.01 ;
123+ result = { extracted : out . invoice , matches } ;
133124 }
134125 } catch {
135- /* fail open — narration already set to the fallback */
126+ /* fail open — result stays { extracted: null } */
136127 }
137128
138- // Pass the input through unchanged (matching runs on the trusted record );
139- // `extracted`/`matches` ride along only for the trace/reveal .
140- return { ... inputData , extracted , matches , narration } ;
129+ await emit ( { type : "intake-result" , payload : result } ) ;
130+ // Matching runs on the trusted record — pass the input through unchanged .
131+ return inputData ;
141132 } ,
142133} ) ;
143134
@@ -152,7 +143,7 @@ const MatchStepOut = MatchResult.merge(Narrated)
152143 . merge ( HumanApproval ) ;
153144const matchingStep = createStep ( {
154145 id : "matching" ,
155- inputSchema : IntakeOut ,
146+ inputSchema : RunInput ,
156147 outputSchema : MatchStepOut ,
157148 execute : async ( { inputData } ) => {
158149 const match = runMatch ( {
0 commit comments