@output.ai/core 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,7 +2,7 @@
2
2
  import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy, continueAsNew } from '@temporalio/workflow';
3
3
  import { validateWorkflow } from './validations/static.js';
4
4
  import { validateWithSchema } from './validations/runtime.js';
5
- import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
5
+ import { SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS, METADATA_ACCESS_SYMBOL } from '#consts';
6
6
  import { deepMerge, mergeActivityOptions, setMetadata } from '#utils';
7
7
  import { FatalError, ValidationError } from '#errors';
8
8
 
@@ -93,47 +93,59 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
93
93
  activityOptions: memo.activityOptions ?? activityOptions // Also preserve the original activity options
94
94
  } );
95
95
 
96
- // validation comes after setting memo to have that info already set for interceptor even if validations fail
97
- validateWithSchema( inputSchema, input, `Workflow ${name} input` );
96
+ // Run the internal activity to retrieve the workflow trace destinations (if it is root workflow, not nested)
97
+ const traceDestinations = isRoot ? ( await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( { startTime, workflowId, workflowName: name } ) ) : null;
98
+ const traceObject = { trace: { destinations: traceDestinations } };
98
99
 
99
- // binds the methods called in the code that Webpack loader will add, they will exposed via "this"
100
- const output = await fn.call( {
101
- invokeStep: async ( stepName, input, options ) => steps[`${name}#${stepName}`]( input, options ),
102
- invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
103
- invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${name}#${evaluatorName}`]( input, options ),
100
+ try {
101
+ // validation comes after setting memo to have that info already set for interceptor even if validations fail
102
+ validateWithSchema( inputSchema, input, `Workflow ${name} input` );
104
103
 
105
- /**
106
- * Start a child workflow
107
- *
108
- * @param {string} childName
109
- * @param {unknown} input
110
- * @param {object} extra
111
- * @param {boolean} extra.detached
112
- * @param {import('@temporalio/workflow').ActivityOptions} extra.options
113
- * @returns
114
- */
115
- startWorkflow: async ( childName, input, extra = {} ) =>
116
- executeChild( childName, {
117
- args: input ? [ input ] : [],
118
- workflowId: `${workflowId}-${childName}-${uuid4()}`,
119
- parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
120
- memo: {
121
- executionContext,
122
- parentId: workflowId,
123
- // new configuration for activities of the child workflow, this will be omitted so it will use what that workflow have defined
124
- ...( extra?.options && { activityOptions: mergeActivityOptions( activityOptions, extra.options ) } )
125
- }
126
- } )
127
- }, input, context );
128
-
129
- validateWithSchema( outputSchema, output, `Workflow ${name} output` );
130
-
131
- if ( isRoot ) {
132
- const destinations = await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( { startTime, workflowId, workflowName: name } );
133
- return { output, trace: { destinations } };
134
- }
104
+ // binds the methods called in the code that Webpack loader will add, they will exposed via "this"
105
+ const output = await fn.call( {
106
+ invokeStep: async ( stepName, input, options ) => steps[`${name}#${stepName}`]( input, options ),
107
+ invokeSharedStep: async ( stepName, input, options ) => steps[`${SHARED_STEP_PREFIX}#${stepName}`]( input, options ),
108
+ invokeEvaluator: async ( evaluatorName, input, options ) => steps[`${name}#${evaluatorName}`]( input, options ),
109
+
110
+ /**
111
+ * Start a child workflow
112
+ *
113
+ * @param {string} childName
114
+ * @param {unknown} input
115
+ * @param {object} extra
116
+ * @param {boolean} extra.detached
117
+ * @param {import('@temporalio/workflow').ActivityOptions} extra.options
118
+ * @returns
119
+ */
120
+ startWorkflow: async ( childName, input, extra = {} ) =>
121
+ executeChild( childName, {
122
+ args: input ? [ input ] : [],
123
+ workflowId: `${workflowId}-${childName}-${uuid4()}`,
124
+ parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
125
+ memo: {
126
+ executionContext,
127
+ parentId: workflowId,
128
+ // new configuration for activities of the child workflow, this will be omitted so it will use what that workflow have defined
129
+ ...( extra?.options && { activityOptions: mergeActivityOptions( activityOptions, extra.options ) } )
130
+ }
131
+ } )
132
+ }, input, context );
133
+
134
+ validateWithSchema( outputSchema, output, `Workflow ${name} output` );
135
135
 
136
- return output;
136
+ if ( isRoot ) {
137
+ // Append the trace info to the result of the workflow
138
+ return { output, ...traceObject };
139
+ }
140
+
141
+ return output;
142
+ } catch ( e ) {
143
+ // Append the trace info as metadata of the error, so it can be read by the interceptor.
144
+ if ( isRoot ) {
145
+ e[METADATA_ACCESS_SYMBOL] = { ...( e[METADATA_ACCESS_SYMBOL] ?? {} ), ...traceObject };
146
+ }
147
+ throw e;
148
+ }
137
149
  };
138
150
 
139
151
  setMetadata( wrapper, { name, description, inputSchema, outputSchema } );
@@ -6,71 +6,76 @@ import { EOL } from 'node:os';
6
6
 
7
7
  const __dirname = dirname( fileURLToPath( import.meta.url ) );
8
8
 
9
- const PURGE_TEMP_FILES_THRESHOLD = 1000 * 60 * 60 * 24 * 7; // 1 week in milliseconds
9
+ const tempFilesTTL = 1000 * 60 * 60 * 24 * 7; // 1 week in milliseconds
10
10
 
11
- // The path to the project root
12
- const LOCAL_PROJECT_ROOT_PATH = process.argv[2] || process.cwd();
11
+ // Retrieves the caller path from the standard args used to start workflows
12
+ const callerDir = process.argv[2];
13
13
 
14
- // The path to the local trace logs
15
- const LOCAL_TRACE_LOG_PATH = join( LOCAL_PROJECT_ROOT_PATH, 'logs' );
16
-
17
- // The path to the temporary trace logs
18
- const TMP_TRACE_LOG_PATH = join( __dirname, 'temp', 'traces' );
14
+ const tempTraceFilesDir = join( __dirname, 'temp', 'traces' );
19
15
 
20
16
  const accumulate = ( { entry, executionContext: { workflowId, startTime } } ) => {
21
- const path = join( TMP_TRACE_LOG_PATH, `${startTime}_${workflowId}.trace` );
17
+ const path = join( tempTraceFilesDir, `${startTime}_${workflowId}.trace` );
22
18
  appendFileSync( path, JSON.stringify( entry ) + EOL, 'utf-8' );
23
19
  return readFileSync( path, 'utf-8' ).split( EOL ).slice( 0, -1 ).map( v => JSON.parse( v ) );
24
20
  };
25
21
 
26
- const cleanupOldTempFiles = ( threshold = Date.now() - PURGE_TEMP_FILES_THRESHOLD ) =>
27
- readdirSync( TMP_TRACE_LOG_PATH )
22
+ const cleanupOldTempFiles = ( threshold = Date.now() - tempFilesTTL ) =>
23
+ readdirSync( tempTraceFilesDir )
28
24
  .filter( f => +f.split( '_' )[0] < threshold )
29
- .forEach( f => rmSync( join( TMP_TRACE_LOG_PATH, f ) ) );
25
+ .forEach( f => rmSync( join( tempTraceFilesDir, f ) ) );
30
26
 
31
27
  /**
32
- * Get the host trace log path, which is used for reporting trace locations.
33
- * In containerized environments (e.g., Docker), this can be different from the local path
34
- * to map container paths to host filesystem paths.
35
- * @returns {string} The host trace log path from HOST_TRACE_PATH env var, or local path as fallback
28
+ * Resolves the deep folder structure that stores a workflow trace.
29
+ * @param {string} workflowName - Name of the workflow
30
+ * @returns {string}
36
31
  */
37
- const getHostTraceLogPath = () => {
38
- return process.env.HOST_TRACE_PATH || LOCAL_TRACE_LOG_PATH;
39
- };
40
-
41
- /**
42
- * Init this processor
43
- */
44
- export const init = () => {
45
- mkdirSync( TMP_TRACE_LOG_PATH, { recursive: true } );
46
- cleanupOldTempFiles();
47
- };
32
+ const resolveTraceFolder = workflowName => join( 'runs', workflowName );
48
33
 
49
34
  /**
50
- * Get the local file system path for ALL file I/O operations (read/write)
51
- * Uses the project root path passed as argv[2], falls back to cwd
35
+ * Resolves the local file system path for ALL file I/O operations (read/write)
36
+ * Uses the project root path
52
37
  * @param {string} workflowName - The name of the workflow
53
38
  * @returns {string} The local filesystem path for file operations
54
39
  */
55
- const getLocalOutputDir = workflowName => {
56
- return join( LOCAL_PROJECT_ROOT_PATH, 'logs', 'runs', workflowName );
57
- };
40
+ const resolveIOPath = workflowName => join( callerDir, 'logs', resolveTraceFolder( workflowName ) );
58
41
 
59
42
  /**
60
- * Get the host path for reporting trace file locations to users
61
- * Uses HOST_TRACE_PATH if set (for Docker), otherwise uses project root
43
+ * Resolves the file path to be reported as the trace destination.
44
+ *
45
+ * Considering that in containerized environments (e.g., Docker), the file path might differ from the host machine,
46
+ * this value takes in consideration the TRACE_HOST_PATH env variable instead of the local filesystem to mount
47
+ * the final file path.
48
+ *
49
+ * If the env variable is not present, it falls back to the same value used to write files locally.
50
+ *
62
51
  * @param {string} workflowName - The name of the workflow
63
- * @returns {string} The path to report to users/API
52
+ * @returns {string} The path to report, reflecting the actual filesystem
64
53
  */
65
- const getReportOutputDir = workflowName => {
66
- return join( getHostTraceLogPath(), 'runs', workflowName );
67
- };
54
+ const resolveReportPath = workflowName => process.env.TRACE_HOST_PATH ?
55
+ join( process.env.TRACE_HOST_PATH, resolveTraceFolder( workflowName ) ) :
56
+ resolveIOPath( workflowName );
68
57
 
69
- const buildOutputFileName = ( { startTime, workflowId } ) => {
58
+ /**
59
+ * Builds the actual trace filename
60
+ *
61
+ * @param {object} options
62
+ * @param {number} options.startTime
63
+ * @param {string} options.workflowId
64
+ * @returns {string}
65
+ */
66
+ const buildTraceFilename = ( { startTime, workflowId } ) => {
70
67
  const timestamp = new Date( startTime ).toISOString().replace( /[:T.]/g, '-' );
71
68
  return `${timestamp}_${workflowId}.json`;
72
69
  };
73
70
 
71
+ /**
72
+ * Init this processor
73
+ */
74
+ export const init = () => {
75
+ mkdirSync( tempTraceFilesDir, { recursive: true } );
76
+ cleanupOldTempFiles();
77
+ };
78
+
74
79
  /**
75
80
  * Execute this processor:
76
81
  *
@@ -84,24 +89,23 @@ const buildOutputFileName = ( { startTime, workflowId } ) => {
84
89
  export const exec = ( { entry, executionContext } ) => {
85
90
  const { workflowId, workflowName, startTime } = executionContext;
86
91
  const content = buildTraceTree( accumulate( { entry, executionContext } ) );
87
-
88
- // Always use local path for writing files
89
- const dir = getLocalOutputDir( workflowName );
90
- const path = join( dir, buildOutputFileName( { startTime, workflowId } ) );
92
+ const dir = resolveIOPath( workflowName );
93
+ const path = join( dir, buildTraceFilename( { startTime, workflowId } ) );
91
94
 
92
95
  mkdirSync( dir, { recursive: true } );
93
96
  writeFileSync( path, JSON.stringify( content, undefined, 2 ) + EOL, 'utf-8' );
94
97
  };
95
98
 
96
99
  /**
97
- * Returns where the trace is saved as an absolute path
100
+ * Returns where the trace is saved as an absolute path.
101
+ *
102
+ * This uses the optional TRACE_HOST_PATH to return values relative to the host OS, not the container, if applicable.
103
+ *
98
104
  * @param {object} args
99
105
  * @param {string} args.startTime - The start time of the workflow
100
106
  * @param {string} args.workflowId - The id of the workflow execution
101
107
  * @param {string} args.workflowName - The name of the workflow
102
108
  * @returns {string} The absolute path where the trace will be saved
103
109
  */
104
- export const getDestination = ( { startTime, workflowId, workflowName } ) => {
105
- // Use report path for reporting to users/API
106
- return join( getReportOutputDir( workflowName ), buildOutputFileName( { workflowId, startTime } ) );
107
- };
110
+ export const getDestination = ( { startTime, workflowId, workflowName } ) =>
111
+ join( resolveReportPath( workflowName ), buildTraceFilename( { workflowId, startTime } ) );
@@ -29,7 +29,7 @@ describe( 'tracing/processors/local', () => {
29
29
  vi.clearAllMocks();
30
30
  store.files.clear();
31
31
  process.argv[2] = '/tmp/project';
32
- delete process.env.HOST_TRACE_PATH; // Clear HOST_TRACE_PATH for clean tests
32
+ delete process.env.TRACE_HOST_PATH; // Clear TRACE_HOST_PATH for clean tests
33
33
  } );
34
34
 
35
35
  it( 'init(): creates temp dir and cleans up old files', async () => {
@@ -62,7 +62,7 @@ describe( 'tracing/processors/local', () => {
62
62
 
63
63
  expect( writeFileSyncMock ).toHaveBeenCalledTimes( 3 );
64
64
  const [ writtenPath, content ] = writeFileSyncMock.mock.calls.at( -1 );
65
- // Changed: Now uses process.cwd() + '/logs' fallback when HOST_TRACE_PATH not set
65
+ // Changed: Now uses process.cwd() + '/logs' fallback when TRACE_HOST_PATH not set
66
66
  expect( writtenPath ).toMatch( /\/runs\/WF\// );
67
67
  expect( JSON.parse( content.trim() ).count ).toBe( 3 );
68
68
  } );
@@ -81,11 +81,11 @@ describe( 'tracing/processors/local', () => {
81
81
  expect( destination ).toContain( '/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
82
82
  } );
83
83
 
84
- it( 'exec(): writes to container path regardless of HOST_TRACE_PATH', async () => {
84
+ it( 'exec(): writes to container path regardless of TRACE_HOST_PATH', async () => {
85
85
  const { exec, init } = await import( './index.js' );
86
86
 
87
- // Set HOST_TRACE_PATH to simulate Docker environment
88
- process.env.HOST_TRACE_PATH = '/host/path/logs';
87
+ // Set TRACE_HOST_PATH to simulate Docker environment
88
+ process.env.TRACE_HOST_PATH = '/host/path/logs';
89
89
 
90
90
  init();
91
91
 
@@ -97,16 +97,16 @@ describe( 'tracing/processors/local', () => {
97
97
  expect( writeFileSyncMock ).toHaveBeenCalledTimes( 1 );
98
98
  const [ writtenPath ] = writeFileSyncMock.mock.calls.at( -1 );
99
99
 
100
- // Should write to process.cwd()/logs, NOT to HOST_TRACE_PATH
100
+ // Should write to process.cwd()/logs, NOT to TRACE_HOST_PATH
101
101
  expect( writtenPath ).not.toContain( '/host/path/logs' );
102
102
  expect( writtenPath ).toMatch( /logs\/runs\/WF\// );
103
103
  } );
104
104
 
105
- it( 'getDestination(): returns HOST_TRACE_PATH when set', async () => {
105
+ it( 'getDestination(): returns TRACE_HOST_PATH when set', async () => {
106
106
  const { getDestination } = await import( './index.js' );
107
107
 
108
- // Set HOST_TRACE_PATH to simulate Docker environment
109
- process.env.HOST_TRACE_PATH = '/host/path/logs';
108
+ // Set TRACE_HOST_PATH to simulate Docker environment
109
+ process.env.TRACE_HOST_PATH = '/host/path/logs';
110
110
 
111
111
  const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
112
112
  const workflowId = 'workflow-id-123';
@@ -114,15 +114,15 @@ describe( 'tracing/processors/local', () => {
114
114
 
115
115
  const destination = getDestination( { startTime, workflowId, workflowName } );
116
116
 
117
- // Should return HOST_TRACE_PATH-based path for reporting
117
+ // Should return TRACE_HOST_PATH-based path for reporting
118
118
  expect( destination ).toBe( '/host/path/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
119
119
  } );
120
120
 
121
121
  it( 'separation of write and report paths works correctly', async () => {
122
122
  const { exec, getDestination, init } = await import( './index.js' );
123
123
 
124
- // Set HOST_TRACE_PATH to simulate Docker environment
125
- process.env.HOST_TRACE_PATH = '/Users/ben/project/logs';
124
+ // Set TRACE_HOST_PATH to simulate Docker environment
125
+ process.env.TRACE_HOST_PATH = '/Users/ben/project/logs';
126
126
 
127
127
  init();
128
128
 
@@ -142,7 +142,7 @@ describe( 'tracing/processors/local', () => {
142
142
  expect( writtenPath ).not.toContain( '/Users/ben/project' );
143
143
  expect( writtenPath ).toMatch( /logs\/runs\/test-workflow\// );
144
144
 
145
- // Verify report path uses HOST_TRACE_PATH
145
+ // Verify report path uses TRACE_HOST_PATH
146
146
  expect( destination ).toBe( '/Users/ben/project/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
147
147
  } );
148
148
  } );
@@ -2,6 +2,7 @@
2
2
  import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew } from '@temporalio/workflow';
3
3
  import { memoToHeaders } from '../sandboxed_utils.js';
4
4
  import { mergeActivityOptions } from '#utils';
5
+ import { METADATA_ACCESS_SYMBOL } from '#consts';
5
6
  // this is a dynamic generated file with activity configs overwrites
6
7
  import stepOptions from '../temp/__activity_options.js';
7
8
 
@@ -37,15 +38,28 @@ class WorkflowExecutionInterceptor {
37
38
  sinks.trace.addWorkflowEventEnd( output );
38
39
  return output;
39
40
  } catch ( error ) {
40
- /* When the error is ContinueAsNew it represents the point where the actual workflow code was
41
- delegated to another run. In this case the result in the traces will be the string below and
42
- a new trace file will be generated */
41
+ /*
42
+ * When the error is a ContinueAsNew instance, it represents the point where the actual workflow code was
43
+ * delegated to another run. In this case the result in the traces will be the string below and
44
+ * a new trace file will be generated
45
+ */
43
46
  if ( error instanceof ContinueAsNew ) {
44
47
  sinks.trace.addWorkflowEventEnd( '<continued_as_new>' );
45
48
  throw error;
46
49
  }
50
+
47
51
  sinks.trace.addWorkflowEventError( error );
48
- throw new ApplicationFailure( error.message, error.constructor.name );
52
+ const failure = new ApplicationFailure( error.message, error.constructor.name );
53
+
54
+ /*
55
+ * If intercepted error has metadata, set it to .details property of Temporal's ApplicationFailure instance.
56
+ * This make it possible for this information be retrieved by Temporal's client instance.
57
+ * Ref: https://typescript.temporal.io/api/classes/common.ApplicationFailure#details
58
+ */
59
+ if ( error[METADATA_ACCESS_SYMBOL] ) {
60
+ failure.details = [ error[METADATA_ACCESS_SYMBOL] ];
61
+ }
62
+ throw failure;
49
63
  }
50
64
  }
51
65
  };