@output.ai/core 0.0.15 → 0.1.0
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/bin/worker.sh +1 -1
- package/package.json +17 -10
- package/src/configs.js +1 -6
- package/src/configs.spec.js +2 -50
- package/src/consts.js +6 -8
- package/src/index.d.ts +169 -7
- package/src/index.js +18 -1
- package/src/interface/evaluator.js +146 -0
- package/src/interface/step.js +4 -9
- package/src/interface/{schema_utils.js → validations/runtime.js} +0 -14
- package/src/interface/validations/runtime.spec.js +29 -0
- package/src/interface/validations/schema_utils.js +8 -0
- package/src/interface/validations/static.js +13 -1
- package/src/interface/validations/static.spec.js +29 -1
- package/src/interface/webhook.js +16 -4
- package/src/interface/workflow.js +32 -54
- package/src/internal_activities/index.js +16 -12
- package/src/tracing/index.d.ts +47 -0
- package/src/tracing/index.js +154 -0
- package/src/tracing/index.private.spec.js +84 -0
- package/src/tracing/index.public.spec.js +86 -0
- package/src/tracing/tracer_tree.js +83 -0
- package/src/tracing/tracer_tree.spec.js +115 -0
- package/src/tracing/utils.js +21 -0
- package/src/tracing/utils.spec.js +14 -0
- package/src/worker/catalog_workflow/catalog.js +19 -10
- package/src/worker/index.js +1 -5
- package/src/worker/interceptors/activity.js +28 -10
- package/src/worker/interceptors/workflow.js +19 -1
- package/src/worker/loader.js +6 -6
- package/src/worker/loader.spec.js +6 -9
- package/src/worker/sinks.js +56 -10
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +35 -4
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +12 -4
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -4
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +13 -4
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +16 -2
- package/src/worker/webpack_loaders/workflow_rewriter/tools.js +46 -13
- package/src/worker/webpack_loaders/workflow_rewriter/tools.spec.js +20 -2
- package/src/worker/tracer/index.js +0 -75
- package/src/worker/tracer/index.test.js +0 -103
- package/src/worker/tracer/tracer_tree.js +0 -84
- package/src/worker/tracer/tracer_tree.test.js +0 -115
- /package/src/{worker/async_storage.js → async_storage.js} +0 -0
- /package/src/interface/{schema_utils.spec.js → validations/schema_utils.spec.js} +0 -0
|
@@ -16,6 +16,9 @@ describe( 'collect_target_imports', () => {
|
|
|
16
16
|
'export const StepA = step({ name: "step.a" })',
|
|
17
17
|
'export const StepB = step({ name: "step.b" })'
|
|
18
18
|
].join( '\n' ) );
|
|
19
|
+
writeFileSync( join( dir, 'evaluators.js' ), [
|
|
20
|
+
'export const EvalA = evaluator({ name: "eval.a" })'
|
|
21
|
+
].join( '\n' ) );
|
|
19
22
|
writeFileSync( join( dir, 'workflow.js' ), [
|
|
20
23
|
'export const FlowA = workflow({ name: "flow.a" })',
|
|
21
24
|
'export default workflow({ name: "flow.def" })'
|
|
@@ -23,16 +26,18 @@ describe( 'collect_target_imports', () => {
|
|
|
23
26
|
|
|
24
27
|
const source = [
|
|
25
28
|
'import { StepA } from "./steps.js";',
|
|
29
|
+
'import { EvalA } from "./evaluators.js";',
|
|
26
30
|
'import WF, { FlowA } from "./workflow.js";',
|
|
27
31
|
'const x = 1;'
|
|
28
32
|
].join( '\n' );
|
|
29
33
|
|
|
30
34
|
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
31
|
-
const { stepImports, flowImports } = collectTargetImports(
|
|
35
|
+
const { stepImports, evaluatorImports, flowImports } = collectTargetImports(
|
|
32
36
|
ast,
|
|
33
37
|
dir,
|
|
34
|
-
{ stepsNameCache: new Map(), workflowNameCache: new Map() }
|
|
38
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
35
39
|
);
|
|
40
|
+
expect( evaluatorImports ).toEqual( [ { localName: 'EvalA', evaluatorName: 'eval.a' } ] );
|
|
36
41
|
|
|
37
42
|
expect( stepImports ).toEqual( [ { localName: 'StepA', stepName: 'step.a' } ] );
|
|
38
43
|
expect( flowImports ).toEqual( [
|
|
@@ -48,20 +53,23 @@ describe( 'collect_target_imports', () => {
|
|
|
48
53
|
it( 'collects CJS requires and removes declarators (steps + default workflow)', () => {
|
|
49
54
|
const dir = mkdtempSync( join( tmpdir(), 'collect-cjs-' ) );
|
|
50
55
|
writeFileSync( join( dir, 'steps.js' ), 'export const StepB = step({ name: "step.b" })\n' );
|
|
56
|
+
writeFileSync( join( dir, 'evaluators.js' ), 'export const EvalB = evaluator({ name: "eval.b" })\n' );
|
|
51
57
|
writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: "flow.c" })\n' );
|
|
52
58
|
|
|
53
59
|
const source = [
|
|
54
60
|
'const { StepB } = require("./steps.js");',
|
|
61
|
+
'const { EvalB } = require("./evaluators.js");',
|
|
55
62
|
'const WF = require("./workflow.js");',
|
|
56
63
|
'const obj = {};'
|
|
57
64
|
].join( '\n' );
|
|
58
65
|
|
|
59
66
|
const ast = makeAst( source, join( dir, 'file.js' ) );
|
|
60
|
-
const { stepImports, flowImports } = collectTargetImports(
|
|
67
|
+
const { stepImports, evaluatorImports, flowImports } = collectTargetImports(
|
|
61
68
|
ast,
|
|
62
69
|
dir,
|
|
63
|
-
{ stepsNameCache: new Map(), workflowNameCache: new Map() }
|
|
70
|
+
{ stepsNameCache: new Map(), evaluatorsNameCache: new Map(), workflowNameCache: new Map() }
|
|
64
71
|
);
|
|
72
|
+
expect( evaluatorImports ).toEqual( [ { localName: 'EvalB', evaluatorName: 'eval.b' } ] );
|
|
65
73
|
|
|
66
74
|
expect( stepImports ).toEqual( [ { localName: 'StepB', stepName: 'step.b' } ] );
|
|
67
75
|
expect( flowImports ).toEqual( [ { localName: 'WF', workflowName: 'flow.c' } ] );
|
|
@@ -10,6 +10,7 @@ const generate = generatorModule.default ?? generatorModule;
|
|
|
10
10
|
|
|
11
11
|
// Caches to avoid re-reading files during a build
|
|
12
12
|
const stepsNameCache = new Map(); // path -> Map<exported, stepName>
|
|
13
|
+
const evaluatorsNameCache = new Map(); // path -> Map<exported, evaluatorName>
|
|
13
14
|
const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exported, flowName> }
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -25,20 +26,20 @@ const workflowNameCache = new Map(); // path -> { default?: name, named: Map<exp
|
|
|
25
26
|
export default function stepImportRewriterAstLoader( source, inputMap ) {
|
|
26
27
|
this.cacheable?.( true );
|
|
27
28
|
const callback = this.async?.() ?? this.callback;
|
|
28
|
-
const cache = { stepsNameCache, workflowNameCache };
|
|
29
|
+
const cache = { stepsNameCache, evaluatorsNameCache, workflowNameCache };
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
32
|
const filename = this.resourcePath;
|
|
32
33
|
const ast = parse( String( source ), filename );
|
|
33
34
|
const fileDir = dirname( filename );
|
|
34
|
-
const { stepImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
35
|
+
const { stepImports, evaluatorImports, flowImports } = collectTargetImports( ast, fileDir, cache );
|
|
35
36
|
|
|
36
37
|
// No imports
|
|
37
|
-
if (
|
|
38
|
+
if ( [].concat( stepImports, evaluatorImports, flowImports ).length === 0 ) {
|
|
38
39
|
return callback( null, source, inputMap );
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const rewrote = rewriteFnBodies( ast, stepImports, flowImports );
|
|
42
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } );
|
|
42
43
|
// No edits performed
|
|
43
44
|
if ( !rewrote ) {
|
|
44
45
|
return callback( null, source, inputMap );
|
|
@@ -15,12 +15,14 @@ const traverse = traverseModule.default ?? traverseModule;
|
|
|
15
15
|
* `this.invokeStep('name', ...)` and `FlowY(...)` with
|
|
16
16
|
* `this.startWorkflow('name', ...)`.
|
|
17
17
|
*
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {Array<{localName:string,
|
|
18
|
+
* @param {object} params
|
|
19
|
+
* @param {import('@babel/types').File} params.ast - Parsed file AST.
|
|
20
|
+
* @param {Array<{localName:string,stepName:string}>} params.stepImports - Step imports.
|
|
21
|
+
* @param {Array<{localName:string,evaluatorName:string}>} params.evaluatorImports - Evaluator imports.
|
|
22
|
+
* @param {Array<{localName:string,workflowName:string}>} params.flowImports - Workflow imports.
|
|
21
23
|
* @returns {boolean} True if the AST was modified; false otherwise.
|
|
22
24
|
*/
|
|
23
|
-
export default function rewriteFnBodies( ast, stepImports, flowImports ) {
|
|
25
|
+
export default function rewriteFnBodies( { ast, stepImports, evaluatorImports, flowImports } ) {
|
|
24
26
|
const state = { rewrote: false };
|
|
25
27
|
traverse( ast, {
|
|
26
28
|
ObjectProperty: path => {
|
|
@@ -56,6 +58,13 @@ export default function rewriteFnBodies( ast, stepImports, flowImports ) {
|
|
|
56
58
|
state.rewrote = true;
|
|
57
59
|
return; // Stop after rewriting as step call
|
|
58
60
|
}
|
|
61
|
+
const evaluator = evaluatorImports.find( x => x.localName === callee.name );
|
|
62
|
+
if ( evaluator ) {
|
|
63
|
+
const args = cPath.node.arguments;
|
|
64
|
+
cPath.replaceWith( createThisMethodCall( 'invokeEvaluator', evaluator.evaluatorName, args ) );
|
|
65
|
+
state.rewrote = true;
|
|
66
|
+
return; // Stop after rewriting as evaluator call
|
|
67
|
+
}
|
|
59
68
|
const flow = flowImports.find( x => x.localName === callee.name );
|
|
60
69
|
if ( flow ) {
|
|
61
70
|
const args = cPath.node.arguments;
|
|
@@ -16,17 +16,31 @@ describe( 'rewrite_fn_bodies', () => {
|
|
|
16
16
|
const stepImports = [ { localName: 'StepA', stepName: 'step.a' } ];
|
|
17
17
|
const flowImports = [ { localName: 'FlowB', workflowName: 'flow.b' } ];
|
|
18
18
|
|
|
19
|
-
const rewrote = rewriteFnBodies( ast, stepImports, flowImports );
|
|
19
|
+
const rewrote = rewriteFnBodies( { ast, stepImports, evaluatorImports: [], flowImports } );
|
|
20
20
|
expect( rewrote ).toBe( true );
|
|
21
21
|
|
|
22
22
|
const code = ast.program.body.map( n => n.type ).length; // smoke: ast mutated
|
|
23
23
|
expect( code ).toBeGreaterThan( 0 );
|
|
24
24
|
} );
|
|
25
25
|
|
|
26
|
+
it( 'rewrites evaluator calls to this.invokeEvaluator', () => {
|
|
27
|
+
const src = [
|
|
28
|
+
'const obj = {',
|
|
29
|
+
' fn: async (x) => {',
|
|
30
|
+
' EvalA(3);',
|
|
31
|
+
' }',
|
|
32
|
+
'}'
|
|
33
|
+
].join( '\n' );
|
|
34
|
+
const ast = parse( src, 'file.js' );
|
|
35
|
+
const evaluatorImports = [ { localName: 'EvalA', evaluatorName: 'eval.a' } ];
|
|
36
|
+
const rewrote = rewriteFnBodies( { ast, stepImports: [], evaluatorImports, flowImports: [] } );
|
|
37
|
+
expect( rewrote ).toBe( true );
|
|
38
|
+
} );
|
|
39
|
+
|
|
26
40
|
it( 'does nothing when no matching calls are present', () => {
|
|
27
41
|
const src = [ 'const obj = { fn: function() { other(); } }' ].join( '\n' );
|
|
28
42
|
const ast = parse( src, 'file.js' );
|
|
29
|
-
const rewrote = rewriteFnBodies( ast, [], [] );
|
|
43
|
+
const rewrote = rewriteFnBodies( { ast, stepImports: [], evaluatorImports: [], flowImports: [] } );
|
|
30
44
|
expect( rewrote ).toBe( false );
|
|
31
45
|
} );
|
|
32
46
|
} );
|
|
@@ -106,6 +106,13 @@ export const toFunctionExpression = arrow => {
|
|
|
106
106
|
*/
|
|
107
107
|
export const isStepsPath = value => /(^|\/)steps\.js$/.test( value );
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Check if a module specifier or request string points to evaluators.js.
|
|
111
|
+
* @param {string} value - Module path or request string.
|
|
112
|
+
* @returns {boolean} True if it matches evaluators.js.
|
|
113
|
+
*/
|
|
114
|
+
export const isEvaluatorsPath = value => /(^|\/)evaluators\.js$/.test( value );
|
|
115
|
+
|
|
109
116
|
/**
|
|
110
117
|
* Check if a module specifier or request string points to workflow.js.
|
|
111
118
|
* @param {string} value - Module path or request string.
|
|
@@ -165,14 +172,18 @@ export const resolveNameFromOptions = ( optionsNode, consts, errorMessagePrefix
|
|
|
165
172
|
};
|
|
166
173
|
|
|
167
174
|
/**
|
|
168
|
-
* Build a map
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* @param {
|
|
172
|
-
* @
|
|
173
|
-
* @
|
|
175
|
+
* Build a map of exported component identifiers to declared names by scanning a module.
|
|
176
|
+
* Coerces only static, analyzable forms: `export const X = callee({ name: '...' })`.
|
|
177
|
+
*
|
|
178
|
+
* @param {object} params
|
|
179
|
+
* @param {string} params.path - Absolute path to the module file.
|
|
180
|
+
* @param {Map<string, Map<string,string>>} params.cache - Cache for memoizing results by file path.
|
|
181
|
+
* @param {('step'|'evaluator')} params.calleeName - Factory function identifier to match.
|
|
182
|
+
* @param {string} params.invalidMessagePrefix - Prefix used in thrown errors when name is invalid.
|
|
183
|
+
* @returns {Map<string,string>} Map of `exportedIdentifier` -> `declaredName`.
|
|
184
|
+
* @throws {Error} When names are missing, dynamic, or otherwise non-static.
|
|
174
185
|
*/
|
|
175
|
-
|
|
186
|
+
const buildComponentNameMap = ( { path, cache, calleeName, invalidMessagePrefix } ) => {
|
|
176
187
|
if ( cache.has( path ) ) {
|
|
177
188
|
return cache.get( path );
|
|
178
189
|
}
|
|
@@ -180,24 +191,46 @@ export const buildStepsNameMap = ( path, cache ) => {
|
|
|
180
191
|
const ast = parse( text, path );
|
|
181
192
|
const consts = extractTopLevelStringConsts( ast );
|
|
182
193
|
|
|
183
|
-
const
|
|
194
|
+
const result = ast.program.body
|
|
184
195
|
.filter( node => isExportNamedDeclaration( node ) && isVariableDeclaration( node.declaration ) )
|
|
185
196
|
.reduce( ( map, node ) => {
|
|
186
|
-
|
|
187
197
|
node.declaration.declarations
|
|
188
|
-
.filter( dec => isIdentifier( dec.id ) && isCallExpression( dec.init ) && isIdentifier( dec.init.callee, { name:
|
|
198
|
+
.filter( dec => isIdentifier( dec.id ) && isCallExpression( dec.init ) && isIdentifier( dec.init.callee, { name: calleeName } ) )
|
|
189
199
|
.map( dec => [
|
|
190
200
|
dec,
|
|
191
|
-
resolveNameFromOptions( dec.init.arguments[0], consts,
|
|
201
|
+
resolveNameFromOptions( dec.init.arguments[0], consts, `${invalidMessagePrefix} ${path} for "${dec.id.name}"` )
|
|
192
202
|
] )
|
|
193
203
|
.forEach( ( [ dec, name ] ) => map.set( dec.id.name, name ) );
|
|
194
204
|
return map;
|
|
195
205
|
}, new Map() );
|
|
196
206
|
|
|
197
|
-
cache.set( path,
|
|
198
|
-
return
|
|
207
|
+
cache.set( path, result );
|
|
208
|
+
return result;
|
|
199
209
|
};
|
|
200
210
|
|
|
211
|
+
export const buildStepsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
212
|
+
path,
|
|
213
|
+
cache,
|
|
214
|
+
calleeName: 'step',
|
|
215
|
+
invalidMessagePrefix: 'Invalid step name in'
|
|
216
|
+
} );
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build a map from exported evaluator identifier to declared evaluator name.
|
|
220
|
+
* Parses `evaluators.js` for `export const X = evaluator({ name: '...' })`.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} path - Absolute path to the evaluators module file.
|
|
223
|
+
* @param {Map<string, Map<string,string>>} cache - Cache of computed evaluator name maps.
|
|
224
|
+
* @returns {Map<string,string>} Exported identifier -> evaluator name.
|
|
225
|
+
* @throws {Error} When a evaluator name is invalid (non-static or missing).
|
|
226
|
+
*/
|
|
227
|
+
export const buildEvaluatorsNameMap = ( path, cache ) => buildComponentNameMap( {
|
|
228
|
+
path,
|
|
229
|
+
cache,
|
|
230
|
+
calleeName: 'evaluator',
|
|
231
|
+
invalidMessagePrefix: 'Invalid evaluator name in'
|
|
232
|
+
} );
|
|
233
|
+
|
|
201
234
|
/**
|
|
202
235
|
* Build a structure with default and named workflow names from a workflow module.
|
|
203
236
|
* Extracts names from `workflow({ name: '...' })` calls.
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
createThisMethodCall,
|
|
16
16
|
resolveNameFromOptions,
|
|
17
17
|
buildStepsNameMap,
|
|
18
|
-
buildWorkflowNameMap
|
|
18
|
+
buildWorkflowNameMap,
|
|
19
|
+
buildEvaluatorsNameMap
|
|
19
20
|
} from './tools.js';
|
|
20
21
|
|
|
21
22
|
describe( 'workflow_rewriter tools', () => {
|
|
@@ -72,6 +73,23 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
72
73
|
rmSync( dir, { recursive: true, force: true } );
|
|
73
74
|
} );
|
|
74
75
|
|
|
76
|
+
it( 'buildEvaluatorsNameMap: reads names from evaluators module and caches result', () => {
|
|
77
|
+
const dir = mkdtempSync( join( tmpdir(), 'tools-evals-' ) );
|
|
78
|
+
const evalsPath = join( dir, 'evaluators.js' );
|
|
79
|
+
writeFileSync( evalsPath, [
|
|
80
|
+
'export const EvalA = evaluator({ name: "eval.a" })',
|
|
81
|
+
'export const EvalB = evaluator({ name: "eval.b" })'
|
|
82
|
+
].join( '\n' ) );
|
|
83
|
+
const cache = new Map();
|
|
84
|
+
const map1 = buildEvaluatorsNameMap( evalsPath, cache );
|
|
85
|
+
expect( map1.get( 'EvalA' ) ).toBe( 'eval.a' );
|
|
86
|
+
expect( map1.get( 'EvalB' ) ).toBe( 'eval.b' );
|
|
87
|
+
expect( cache.get( evalsPath ) ).toBe( map1 );
|
|
88
|
+
const map2 = buildEvaluatorsNameMap( evalsPath, cache );
|
|
89
|
+
expect( map2 ).toBe( map1 );
|
|
90
|
+
rmSync( dir, { recursive: true, force: true } );
|
|
91
|
+
} );
|
|
92
|
+
|
|
75
93
|
it( 'getLocalNameFromDestructuredProperty: handles { a }, { a: b }, { a: b = 1 }', () => {
|
|
76
94
|
// { a }
|
|
77
95
|
const p1 = t.objectProperty( t.identifier( 'a' ), t.identifier( 'a' ), false, true );
|
|
@@ -88,7 +106,7 @@ describe( 'workflow_rewriter tools', () => {
|
|
|
88
106
|
} );
|
|
89
107
|
|
|
90
108
|
it( 'buildWorkflowNameMap: reads named and default workflow names and caches', () => {
|
|
91
|
-
const dir = mkdtempSync( join( tmpdir(), 'tools-
|
|
109
|
+
const dir = mkdtempSync( join( tmpdir(), 'tools-output-' ) );
|
|
92
110
|
const wfPath = join( dir, 'workflow.js' );
|
|
93
111
|
writeFileSync( wfPath, [
|
|
94
112
|
'export const FlowA = workflow({ name: "flow.a" })',
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { Storage } from '../async_storage.js';
|
|
2
|
-
import { mkdirSync, existsSync, readdirSync, appendFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { EOL } from 'os';
|
|
5
|
-
import { buildLogTree } from './tracer_tree.js';
|
|
6
|
-
import { tracing as tracingConfig } from '#configs';
|
|
7
|
-
|
|
8
|
-
const callerDir = process.argv[2];
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Appends new information to a file
|
|
12
|
-
*
|
|
13
|
-
* Information has to be a JSON
|
|
14
|
-
*
|
|
15
|
-
* File is encoded in utf-8
|
|
16
|
-
*
|
|
17
|
-
* @param {string} path - The full filename
|
|
18
|
-
* @param {object} json - The content
|
|
19
|
-
*/
|
|
20
|
-
const flushEntry = ( path, json ) => appendFileSync( path, JSON.stringify( json ) + EOL, 'utf-8' );
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Add an event to the execution trace file.
|
|
24
|
-
*
|
|
25
|
-
* Events normally are the result of an operation, either a function call or an IO.
|
|
26
|
-
*
|
|
27
|
-
* @param {object} options
|
|
28
|
-
* @param {string} options.lib - The macro part of the platform that triggered the event
|
|
29
|
-
* @param {string} options.event - The name of the event
|
|
30
|
-
* @param {any} [options.input] - The input of the operation
|
|
31
|
-
* @param {any} [options.output] - The output of the operation
|
|
32
|
-
*/
|
|
33
|
-
export function trace( { lib, event, input = undefined, output = undefined } ) {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
|
|
36
|
-
if ( !tracingConfig.enabled ) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const {
|
|
41
|
-
activityId: stepId,
|
|
42
|
-
activityType: stepName,
|
|
43
|
-
workflowId,
|
|
44
|
-
workflowType,
|
|
45
|
-
workflowPath,
|
|
46
|
-
parentWorkflowId,
|
|
47
|
-
rootWorkflowId,
|
|
48
|
-
rootWorkflowType
|
|
49
|
-
} = Storage.load();
|
|
50
|
-
|
|
51
|
-
const entry = { lib, event, input, output, timestamp: now, stepId, stepName, workflowId, workflowType, workflowPath, parentWorkflowId };
|
|
52
|
-
|
|
53
|
-
// test for rootWorkflow to append to the same file as the parent/grandparent
|
|
54
|
-
const outputDir = join( callerDir, 'logs', 'runs', rootWorkflowType ?? workflowType );
|
|
55
|
-
if ( !existsSync( outputDir ) ) {
|
|
56
|
-
mkdirSync( outputDir, { recursive: true } );
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const suffix = `-${rootWorkflowId ?? workflowId}.raw`;
|
|
60
|
-
const logFile = readdirSync( outputDir ).find( f => f.endsWith( suffix ) ) ?? `${new Date( now ).toISOString()}-${suffix}`;
|
|
61
|
-
const logPath = join( outputDir, logFile );
|
|
62
|
-
|
|
63
|
-
flushEntry( logPath, entry );
|
|
64
|
-
buildLogTree( logPath );
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Setup the global tracer function, so it is available to be used by other libraries
|
|
69
|
-
*
|
|
70
|
-
* It will be situated in the global object, under Symbol.for('__trace')
|
|
71
|
-
*
|
|
72
|
-
* @returns {object} The assigned globalThis
|
|
73
|
-
*/
|
|
74
|
-
export const setupGlobalTracer = () =>
|
|
75
|
-
Object.defineProperty( globalThis, Symbol.for( '__trace' ), { value: trace, writable: false, enumerable: false, configurable: false } );
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir, EOL } from 'node:os';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { THIS_LIB_NAME } from '#consts';
|
|
6
|
-
|
|
7
|
-
const createTempDir = () => mkdtempSync( join( tmpdir(), 'flow-sdk-trace-' ) );
|
|
8
|
-
|
|
9
|
-
// Single mocks (configured per-test by mutating the backing objects)
|
|
10
|
-
const mockConfig = { tracing: { enabled: false } };
|
|
11
|
-
vi.mock( '#configs', () => mockConfig );
|
|
12
|
-
|
|
13
|
-
const mockStorageData = {
|
|
14
|
-
activityId: 's1',
|
|
15
|
-
activityType: 'Step 1',
|
|
16
|
-
workflowId: 'wf1',
|
|
17
|
-
workflowType: 'prompt',
|
|
18
|
-
workflowPath: '/workflows/prompt.js',
|
|
19
|
-
parentWorkflowId: undefined,
|
|
20
|
-
rootWorkflowId: undefined,
|
|
21
|
-
rootWorkflowType: undefined
|
|
22
|
-
};
|
|
23
|
-
vi.mock( '../async_storage.js', () => ( {
|
|
24
|
-
Storage: { load: () => mockStorageData }
|
|
25
|
-
} ) );
|
|
26
|
-
|
|
27
|
-
vi.mock( './tracer_tree.js', () => ( { buildLogTree: vi.fn() } ) );
|
|
28
|
-
|
|
29
|
-
describe( 'tracer/index', () => {
|
|
30
|
-
beforeEach( () => {
|
|
31
|
-
vi.resetModules();
|
|
32
|
-
vi.clearAllMocks();
|
|
33
|
-
vi.useFakeTimers();
|
|
34
|
-
vi.setSystemTime( new Date( '2020-01-01T00:00:00.000Z' ) );
|
|
35
|
-
|
|
36
|
-
} );
|
|
37
|
-
|
|
38
|
-
afterEach( () => {
|
|
39
|
-
vi.useRealTimers();
|
|
40
|
-
} );
|
|
41
|
-
|
|
42
|
-
it( 'writes a raw log entry and calls buildLogTree (mocked)', async () => {
|
|
43
|
-
const originalArgv2 = process.argv[2];
|
|
44
|
-
const tmp = createTempDir();
|
|
45
|
-
process.argv[2] = tmp;
|
|
46
|
-
|
|
47
|
-
mockConfig.tracing.enabled = true;
|
|
48
|
-
|
|
49
|
-
const { buildLogTree } = await import( './tracer_tree.js' );
|
|
50
|
-
const { trace } = await import( './index.js' );
|
|
51
|
-
|
|
52
|
-
const input = { foo: 1 };
|
|
53
|
-
trace( { lib: THIS_LIB_NAME, event: 'workflow_start', input, output: null } );
|
|
54
|
-
|
|
55
|
-
expect( buildLogTree ).toHaveBeenCalledTimes( 1 );
|
|
56
|
-
const logPath = buildLogTree.mock.calls[0][0];
|
|
57
|
-
|
|
58
|
-
const raw = readFileSync( logPath, 'utf-8' );
|
|
59
|
-
const [ firstLine ] = raw.split( EOL );
|
|
60
|
-
const entry = JSON.parse( firstLine );
|
|
61
|
-
|
|
62
|
-
expect( entry ).toMatchObject( {
|
|
63
|
-
lib: THIS_LIB_NAME,
|
|
64
|
-
event: 'workflow_start',
|
|
65
|
-
input,
|
|
66
|
-
output: null,
|
|
67
|
-
stepId: 's1',
|
|
68
|
-
stepName: 'Step 1',
|
|
69
|
-
workflowId: 'wf1',
|
|
70
|
-
workflowType: 'prompt',
|
|
71
|
-
workflowPath: '/workflows/prompt.js'
|
|
72
|
-
} );
|
|
73
|
-
expect( typeof entry.timestamp ).toBe( 'number' );
|
|
74
|
-
|
|
75
|
-
rmSync( tmp, { recursive: true, force: true } );
|
|
76
|
-
process.argv[2] = originalArgv2;
|
|
77
|
-
} );
|
|
78
|
-
|
|
79
|
-
it( 'does nothing when tracing is disabled', async () => {
|
|
80
|
-
const originalArgv2 = process.argv[2];
|
|
81
|
-
const tmp = createTempDir();
|
|
82
|
-
process.argv[2] = tmp;
|
|
83
|
-
|
|
84
|
-
mockConfig.tracing.enabled = false;
|
|
85
|
-
const { buildLogTree } = await import( './tracer_tree.js' );
|
|
86
|
-
const { trace } = await import( './index.js' );
|
|
87
|
-
|
|
88
|
-
trace( { lib: THIS_LIB_NAME, event: 'workflow_start', input: {}, output: null } );
|
|
89
|
-
|
|
90
|
-
expect( buildLogTree ).not.toHaveBeenCalled();
|
|
91
|
-
|
|
92
|
-
rmSync( tmp, { recursive: true, force: true } );
|
|
93
|
-
process.argv[2] = originalArgv2;
|
|
94
|
-
} );
|
|
95
|
-
|
|
96
|
-
it( 'setupGlobalTracer installs global symbol', async () => {
|
|
97
|
-
mockConfig.tracing.enabled = false;
|
|
98
|
-
const { setupGlobalTracer } = await import( './index.js' );
|
|
99
|
-
setupGlobalTracer();
|
|
100
|
-
const sym = Symbol.for( '__trace' );
|
|
101
|
-
expect( typeof globalThis[sym] ).toBe( 'function' );
|
|
102
|
-
} );
|
|
103
|
-
} );
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { EOL } from 'os';
|
|
3
|
-
import { THIS_LIB_NAME, TraceEvent } from '#consts';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Sorting function that compares two objects and ASC sort them by either .startedAt or, if not present, .timestamp
|
|
7
|
-
*
|
|
8
|
-
* @param {object} a
|
|
9
|
-
* @param {object} b
|
|
10
|
-
* @returns {number} The sorting result [1,-1]
|
|
11
|
-
*/
|
|
12
|
-
const timestampAscSort = ( a, b ) => {
|
|
13
|
-
if ( a.startedAt ) {
|
|
14
|
-
return a.startedAt > b.startedAt ? 1 : 1;
|
|
15
|
-
}
|
|
16
|
-
return a.timestamp > b.timestamp ? 1 : -1;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Add a member to an array an sort it. It is a mutating method.
|
|
21
|
-
*
|
|
22
|
-
* @param {array} arr - The arr to be changed
|
|
23
|
-
* @param {any} entry - The entry to be added
|
|
24
|
-
* @param {Function} sorter - The sort function to be used (within .filter)
|
|
25
|
-
*/
|
|
26
|
-
const pushSort = ( arr, entry, sorter ) => {
|
|
27
|
-
arr.push( entry );
|
|
28
|
-
arr.sort( sorter );
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Transform the trace file into a tree of events, where nested events are represented as children of parent events.
|
|
33
|
-
* And the events STEP_START/STEP_END and WORKFLOW_START/WORKFLOW_END are combined into single events with start and end timestamps.
|
|
34
|
-
*
|
|
35
|
-
* @param {string} src - The trace src filename
|
|
36
|
-
*/
|
|
37
|
-
export const buildLogTree = src => {
|
|
38
|
-
const content = readFileSync( src, 'utf-8' );
|
|
39
|
-
const entries = content.split( EOL ).slice( 0, -1 ).map( c => JSON.parse( c ) );
|
|
40
|
-
|
|
41
|
-
const stepsMap = new Map();
|
|
42
|
-
const workflowsMap = new Map();
|
|
43
|
-
|
|
44
|
-
// close steps/workflows
|
|
45
|
-
for ( const entry of entries.filter( e => e.lib === THIS_LIB_NAME ) ) {
|
|
46
|
-
const { event, workflowId, workflowType, workflowPath, parentWorkflowId, stepId, stepName, input, output, timestamp } = entry;
|
|
47
|
-
|
|
48
|
-
const baseEntry = { children: [], startedAt: timestamp, workflowId };
|
|
49
|
-
if ( event === TraceEvent.STEP_START ) {
|
|
50
|
-
stepsMap.set( `${workflowId}:${stepId}`, { event: 'step', input, stepId, stepName, ...baseEntry } );
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if ( event === TraceEvent.STEP_END ) {
|
|
54
|
-
Object.assign( stepsMap.get( `${workflowId}:${stepId}` ) ?? {}, { output, endedAt: timestamp } );
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if ( event === TraceEvent.WORKFLOW_START ) {
|
|
58
|
-
workflowsMap.set( workflowId, { event: 'workflow', input, parentWorkflowId, workflowPath, workflowType, ...baseEntry } );
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if ( event === TraceEvent.WORKFLOW_END ) {
|
|
62
|
-
Object.assign( workflowsMap.get( workflowId ) ?? {}, { output, endedAt: timestamp } );
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// insert operations inside steps
|
|
67
|
-
for ( const entry of entries.filter( e => e.lib !== THIS_LIB_NAME ) ) {
|
|
68
|
-
pushSort( stepsMap.get( `${entry.workflowId}:${entry.stepId}` ).children, entry, timestampAscSort );
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// insert steps into workflows
|
|
72
|
-
for ( const step of stepsMap.values() ) {
|
|
73
|
-
pushSort( workflowsMap.get( step.workflowId ).children, step, timestampAscSort );
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// insert children workflows
|
|
77
|
-
for ( const workflow of [ ...workflowsMap.values() ].filter( w => w.parentWorkflowId ) ) {
|
|
78
|
-
pushSort( workflowsMap.get( workflow.parentWorkflowId ).children, workflow, timestampAscSort );
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const rootWorkflow = [ ...workflowsMap.values() ].find( w => !w.parentWorkflowId );
|
|
82
|
-
|
|
83
|
-
writeFileSync( src.replace( /\.raw$/, '.json' ), JSON.stringify( rootWorkflow, undefined, 2 ), 'utf-8' );
|
|
84
|
-
};
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
-
import { mkdtempSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import { EOL } from 'os';
|
|
7
|
-
import { buildLogTree } from './tracer_tree.js';
|
|
8
|
-
import { THIS_LIB_NAME, TraceEvent } from '#consts';
|
|
9
|
-
|
|
10
|
-
const createTempDir = () => mkdtempSync( join( tmpdir(), 'flow-sdk-trace-tree-' ) );
|
|
11
|
-
|
|
12
|
-
describe( 'tracer/tracer_tree', () => {
|
|
13
|
-
it( 'builds a tree JSON from a raw log file', () => {
|
|
14
|
-
const tmp = createTempDir();
|
|
15
|
-
const rawPath = join( tmp, 'run-123.raw' );
|
|
16
|
-
|
|
17
|
-
const entries = [
|
|
18
|
-
// root workflow start
|
|
19
|
-
{
|
|
20
|
-
lib: THIS_LIB_NAME,
|
|
21
|
-
event: TraceEvent.WORKFLOW_START,
|
|
22
|
-
input: { a: 1 },
|
|
23
|
-
output: null,
|
|
24
|
-
timestamp: 1000,
|
|
25
|
-
stepId: undefined,
|
|
26
|
-
stepName: undefined,
|
|
27
|
-
workflowId: 'wf1',
|
|
28
|
-
workflowType: 'prompt',
|
|
29
|
-
workflowPath: '/workflows/prompt.js',
|
|
30
|
-
parentWorkflowId: undefined
|
|
31
|
-
},
|
|
32
|
-
// step start
|
|
33
|
-
{
|
|
34
|
-
lib: THIS_LIB_NAME,
|
|
35
|
-
event: TraceEvent.STEP_START,
|
|
36
|
-
input: { x: 1 },
|
|
37
|
-
output: null,
|
|
38
|
-
timestamp: 2000,
|
|
39
|
-
stepId: 's1',
|
|
40
|
-
stepName: 'Step 1',
|
|
41
|
-
workflowId: 'wf1',
|
|
42
|
-
workflowType: 'prompt',
|
|
43
|
-
workflowPath: '/workflows/prompt.js',
|
|
44
|
-
parentWorkflowId: undefined
|
|
45
|
-
},
|
|
46
|
-
// non-core operation within step
|
|
47
|
-
{
|
|
48
|
-
lib: 'tool',
|
|
49
|
-
event: 'call',
|
|
50
|
-
input: { y: 2 },
|
|
51
|
-
output: { y: 3 },
|
|
52
|
-
timestamp: 3000,
|
|
53
|
-
stepId: 's1',
|
|
54
|
-
stepName: 'Step 1',
|
|
55
|
-
workflowId: 'wf1'
|
|
56
|
-
},
|
|
57
|
-
// step end
|
|
58
|
-
{
|
|
59
|
-
lib: THIS_LIB_NAME,
|
|
60
|
-
event: TraceEvent.STEP_END,
|
|
61
|
-
input: null,
|
|
62
|
-
output: { done: true },
|
|
63
|
-
timestamp: 4000,
|
|
64
|
-
stepId: 's1',
|
|
65
|
-
stepName: 'Step 1',
|
|
66
|
-
workflowId: 'wf1',
|
|
67
|
-
workflowType: 'prompt',
|
|
68
|
-
workflowPath: '/workflows/prompt.js',
|
|
69
|
-
parentWorkflowId: undefined
|
|
70
|
-
},
|
|
71
|
-
// workflow end
|
|
72
|
-
{
|
|
73
|
-
lib: THIS_LIB_NAME,
|
|
74
|
-
event: TraceEvent.WORKFLOW_END,
|
|
75
|
-
input: null,
|
|
76
|
-
output: { ok: true },
|
|
77
|
-
timestamp: 5000,
|
|
78
|
-
stepId: undefined,
|
|
79
|
-
stepName: undefined,
|
|
80
|
-
workflowId: 'wf1',
|
|
81
|
-
workflowType: 'prompt',
|
|
82
|
-
workflowPath: '/workflows/prompt.js',
|
|
83
|
-
parentWorkflowId: undefined
|
|
84
|
-
}
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
writeFileSync( rawPath, entries.map( e => JSON.stringify( e ) ).join( EOL ) + EOL, 'utf-8' );
|
|
88
|
-
|
|
89
|
-
buildLogTree( rawPath );
|
|
90
|
-
|
|
91
|
-
const tree = JSON.parse( readFileSync( rawPath.replace( /.raw$/, '.json' ), 'utf-8' ) );
|
|
92
|
-
|
|
93
|
-
expect( tree.event ).toBe( 'workflow' );
|
|
94
|
-
expect( tree.workflowId ).toBe( 'wf1' );
|
|
95
|
-
expect( tree.workflowType ).toBe( 'prompt' );
|
|
96
|
-
expect( tree.startedAt ).toBe( 1000 );
|
|
97
|
-
expect( tree.endedAt ).toBe( 5000 );
|
|
98
|
-
expect( tree.output ).toEqual( { ok: true } );
|
|
99
|
-
expect( Array.isArray( tree.children ) ).toBe( true );
|
|
100
|
-
expect( tree.children.length ).toBe( 1 );
|
|
101
|
-
|
|
102
|
-
const step = tree.children[0];
|
|
103
|
-
expect( step.event ).toBe( 'step' );
|
|
104
|
-
expect( step.stepId ).toBe( 's1' );
|
|
105
|
-
expect( step.startedAt ).toBe( 2000 );
|
|
106
|
-
expect( step.endedAt ).toBe( 4000 );
|
|
107
|
-
expect( step.output ).toEqual( { done: true } );
|
|
108
|
-
expect( step.children.length ).toBe( 1 );
|
|
109
|
-
expect( step.children[0].lib ).toBe( 'tool' );
|
|
110
|
-
expect( step.children[0].timestamp ).toBe( 3000 );
|
|
111
|
-
|
|
112
|
-
rmSync( tmp, { recursive: true, force: true } );
|
|
113
|
-
} );
|
|
114
|
-
} );
|
|
115
|
-
|
|
File without changes
|