11/**
22 * Benchmark Workflow Execution
33 *
4- * Executes a GraphWorkflowConfig against a single dataset sample by invoking
5- * the existing `graphWorkflow` as a child workflow on the `benchmark-processing`
6- * task queue. This ensures benchmarks test the actual execution path.
4+ * Per-sample dispatcher called from `benchmarkRunWorkflow`. Starts the wrapper
5+ * child workflow `benchmarkSampleWorkflow` (which internally runs the generic
6+ * `graphWorkflow` and writes prediction / persists OCR cache from inside its
7+ * own context) on the `benchmark-processing` task queue. The wrapper returns a
8+ * slim summary so the parent benchmark orchestrator's history stays small.
79 *
810 * NOTE: This module exports a workflow-level helper function (not a pure activity)
911 * because it uses `executeChild` to start child workflows, which is only available
10- * in the Temporal workflow context. The benchmark orchestrator (US-022) calls this
11- * directly from workflow code.
12- *
13- * See feature-docs/003-benchmarking-system/user-stories/US-019-workflow-execution-activity.md
14- * See feature-docs/003-benchmarking-system/REQUIREMENTS.md Section 4.2, 13.1
12+ * in the Temporal workflow context.
1513 */
1614
1715import { executeChild , workflowInfo } from "@temporalio/workflow" ;
18- import {
19- GRAPH_RUNNER_VERSION ,
20- type GraphWorkflowConfig ,
21- type GraphWorkflowInput ,
22- type GraphWorkflowResult ,
23- } from "../graph-workflow-types" ;
16+ import type { BenchmarkSampleWorkflowOutput } from "../benchmark-sample-workflow" ;
17+ import type { GraphWorkflowConfig } from "../graph-workflow-types" ;
2418
2519// ---------------------------------------------------------------------------
2620// Types
2721// ---------------------------------------------------------------------------
2822
2923export interface BenchmarkExecuteInput {
30- /** Unique sample identifier from the dataset manifest */
3124 sampleId : string ;
32-
33- /** The graph workflow configuration to execute */
3425 workflowConfig : GraphWorkflowConfig ;
35-
36- /** SHA-256 hash of the workflow config (for version pinning) */
3726 configHash : string ;
38-
39- /** Paths to input files for this sample (materialized on disk) */
4027 inputPaths : string [ ] ;
41-
42- /** Base directory for writing per-sample output files */
4328 outputBaseDir : string ;
44-
45- /** Additional context to pass to the workflow */
4629 sampleMetadata : Record < string , unknown > ;
47-
48- /** Timeout for the child workflow execution in milliseconds */
30+ /** Directory under which the per-sample prediction JSON should be written. */
31+ predictionOutputDir : string ;
32+ /** When set, wrapper persists OCR response under this benchmark run id. */
33+ persistOcrCache ?: { sourceRunId : string } ;
4934 timeoutMs ?: number ;
50-
51- /** Task queue to route the child workflow to (defaults to benchmark-processing) */
5235 taskQueue ?: string ;
36+ requestId ?: string ;
5337}
5438
5539export interface BenchmarkExecuteOutput {
56- /** The sample ID this result belongs to */
5740 sampleId : string ;
58-
59- /** Whether the workflow execution succeeded */
6041 success : boolean ;
61-
62- /** The workflow result if successful */
63- workflowResult ?: GraphWorkflowResult ;
64-
65- /** Paths to output files produced by the workflow */
42+ /** Path to per-sample prediction JSON written by the wrapper child. */
43+ predictionPath ?: string ;
44+ /** Per-field confidence map flattened from the inner workflow ctx. */
45+ confidenceData ?: Record < string , number | null > ;
46+ /** Output paths reported by the inner workflow ctx. */
6647 outputPaths : string [ ] ;
67-
68- /** Error details if the workflow failed */
69- error ?: {
70- message : string ;
71- failedNodeId ?: string ;
72- type ?: string ;
73- } ;
74-
75- /** Duration of the child workflow execution in milliseconds */
48+ error ?: { message : string ; failedNodeId ?: string ; type ?: string } ;
7649 durationMs : number ;
7750}
7851
@@ -87,13 +60,6 @@ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
8760// Main Function
8861// ---------------------------------------------------------------------------
8962
90- /**
91- * Execute a GraphWorkflowConfig as a child workflow for a single benchmark sample.
92- *
93- * This function is called from the benchmark orchestrator workflow context.
94- * It invokes `graphWorkflow` as a child workflow on the `benchmark-processing`
95- * task queue to ensure isolation from production workloads.
96- */
9763export async function benchmarkExecuteWorkflow (
9864 params : BenchmarkExecuteInput ,
9965) : Promise < BenchmarkExecuteOutput > {
@@ -105,8 +71,11 @@ export async function benchmarkExecuteWorkflow(
10571 inputPaths,
10672 outputBaseDir,
10773 sampleMetadata,
74+ predictionOutputDir,
75+ persistOcrCache,
10876 timeoutMs = DEFAULT_TIMEOUT_MS ,
10977 taskQueue = BENCHMARK_TASK_QUEUE ,
78+ requestId,
11079 } = params ;
11180
11281 const parentWorkflowId = workflowInfo ( ) . workflowId ;
@@ -126,51 +95,25 @@ export async function benchmarkExecuteWorkflow(
12695 ) ;
12796
12897 try {
129- // Build initial context for the child workflow.
130- // The graph workflow nodes expect fields like blobKey, documentId, fileName, etc.
131- // In benchmark mode, we populate these from the materialized input files.
132- const primaryInput = inputPaths [ 0 ] || "" ;
133- const fileName = primaryInput . split ( "/" ) . pop ( ) || "document" ;
134- const lowerName = fileName . toLowerCase ( ) ;
135- const isImage = / \. ( j p g | j p e g | p n g | g i f | b m p | t i f f | w e b p ) $ / i. test ( lowerName ) ;
136- const fileType = isImage ? "image" : "pdf" ;
137- const contentType = isImage
138- ? lowerName . endsWith ( ".png" )
139- ? "image/png"
140- : "image/jpeg"
141- : "application/pdf" ;
142-
143- const initialCtx : Record < string , unknown > = {
144- ...sampleMetadata ,
145- inputPaths,
146- outputBaseDir,
147- sampleId,
148- // Fields expected by graph workflow nodes (e.g., file.prepare)
149- documentId : `benchmark-${ sampleId } ` ,
150- blobKey : primaryInput ,
151- fileName,
152- fileType,
153- contentType,
154- } ;
155-
156- const childWorkflowInput : GraphWorkflowInput = {
157- graph : workflowConfig ,
158- initialCtx,
159- configHash,
160- runnerVersion : GRAPH_RUNNER_VERSION ,
161- parentWorkflowId,
162- } ;
163-
164- // Execute the graphWorkflow as a child workflow on the specified task queue
165- const childResult = ( await executeChild ( "graphWorkflow" , {
166- args : [ childWorkflowInput ] ,
98+ const childResult = ( await executeChild ( "benchmarkSampleWorkflow" , {
99+ args : [
100+ {
101+ sampleId,
102+ workflowConfig,
103+ configHash,
104+ inputPaths,
105+ outputBaseDir,
106+ sampleMetadata,
107+ predictionOutputDir,
108+ persistOcrCache,
109+ parentWorkflowId,
110+ requestId,
111+ } ,
112+ ] ,
167113 taskQueue,
168114 workflowId : childWorkflowId ,
169115 workflowExecutionTimeout : timeoutMs ,
170- } ) ) as GraphWorkflowResult ;
171-
172- // Collect output paths from the workflow context
173- const outputPaths = extractOutputPaths ( childResult . ctx ) ;
116+ } ) ) as BenchmarkSampleWorkflowOutput ;
174117
175118 const durationMs = Date . now ( ) - startTime ;
176119
@@ -179,33 +122,28 @@ export async function benchmarkExecuteWorkflow(
179122 activity : "benchmarkExecuteWorkflow" ,
180123 event : "complete" ,
181124 sampleId,
182- status : childResult . status ,
183- completedNodes : childResult . completedNodes . length ,
184- outputPaths : outputPaths . length ,
125+ status :
126+ childResult . graphStatus ??
127+ ( childResult . success ? "completed" : "failed" ) ,
128+ completedNodes : childResult . completedNodes ?? 0 ,
129+ outputPaths : childResult . outputPaths . length ,
185130 durationMs,
186131 timestamp : new Date ( ) . toISOString ( ) ,
187132 } ) ,
188133 ) ;
189134
190- if ( childResult . status === "failed" ) {
191- return {
192- sampleId,
193- success : false ,
194- workflowResult : childResult ,
195- outputPaths,
196- error : {
197- message : `Workflow completed with status: failed` ,
198- failedNodeId : findFailedNodeId ( childResult ) ,
199- } ,
200- durationMs,
201- } ;
202- }
203-
204135 return {
205136 sampleId,
206- success : true ,
207- workflowResult : childResult ,
208- outputPaths,
137+ success : childResult . success ,
138+ predictionPath : childResult . predictionPath ,
139+ confidenceData : childResult . confidenceData ,
140+ outputPaths : childResult . outputPaths ,
141+ error : childResult . error
142+ ? {
143+ message : childResult . error . message ,
144+ failedNodeId : childResult . error . failedNodeId ,
145+ }
146+ : undefined ,
209147 durationMs,
210148 } ;
211149 } catch ( error ) {
@@ -243,15 +181,11 @@ export async function benchmarkExecuteWorkflow(
243181 } ) ,
244182 ) ;
245183
246- // Return failure result without crashing the parent benchmark workflow
247184 return {
248185 sampleId,
249186 success : false ,
250187 outputPaths : [ ] ,
251- error : {
252- message : errorMessage ,
253- type : errorType ,
254- } ,
188+ error : { message : errorMessage , type : errorType } ,
255189 durationMs,
256190 } ;
257191 }
@@ -261,61 +195,6 @@ export async function benchmarkExecuteWorkflow(
261195// Helpers
262196// ---------------------------------------------------------------------------
263197
264- /**
265- * Extract output file paths from the workflow context.
266- * Looks for common output path patterns in the context.
267- */
268- function extractOutputPaths ( ctx : Record < string , unknown > ) : string [ ] {
269- const paths : string [ ] = [ ] ;
270-
271- // Check for explicit outputPaths in context
272- if ( Array . isArray ( ctx . outputPaths ) ) {
273- for ( const p of ctx . outputPaths ) {
274- if ( typeof p === "string" ) {
275- paths . push ( p ) ;
276- }
277- }
278- }
279-
280- // Check for outputPath (singular) in context
281- if ( typeof ctx . outputPath === "string" ) {
282- paths . push ( ctx . outputPath ) ;
283- }
284-
285- // Check for results that contain file paths
286- if ( Array . isArray ( ctx . results ) ) {
287- for ( const result of ctx . results ) {
288- if ( result && typeof result === "object" && "outputPath" in result ) {
289- const resultObj = result as Record < string , unknown > ;
290- if ( typeof resultObj . outputPath === "string" ) {
291- paths . push ( resultObj . outputPath ) ;
292- }
293- }
294- }
295- }
296-
297- // If no paths found but outputBaseDir is in ctx, use that
298- if ( paths . length === 0 && typeof ctx . outputBaseDir === "string" ) {
299- paths . push ( ctx . outputBaseDir ) ;
300- }
301-
302- return paths ;
303- }
304-
305- /**
306- * Try to extract the failed node ID from a workflow result.
307- */
308- function findFailedNodeId ( result : GraphWorkflowResult ) : string | undefined {
309- // Check if there's error info in the context that might contain a failed node ID
310- if ( result . ctx && typeof result . ctx . failedNodeId === "string" ) {
311- return result . ctx . failedNodeId ;
312- }
313- return undefined ;
314- }
315-
316- /**
317- * Extract error type string from an error.
318- */
319198function extractErrorType ( error : unknown ) : string {
320199 if ( error instanceof Error ) {
321200 if (
0 commit comments