@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.
Files changed (45) hide show
  1. package/bin/worker.sh +1 -1
  2. package/package.json +17 -10
  3. package/src/configs.js +1 -6
  4. package/src/configs.spec.js +2 -50
  5. package/src/consts.js +6 -8
  6. package/src/index.d.ts +169 -7
  7. package/src/index.js +18 -1
  8. package/src/interface/evaluator.js +146 -0
  9. package/src/interface/step.js +4 -9
  10. package/src/interface/{schema_utils.js → validations/runtime.js} +0 -14
  11. package/src/interface/validations/runtime.spec.js +29 -0
  12. package/src/interface/validations/schema_utils.js +8 -0
  13. package/src/interface/validations/static.js +13 -1
  14. package/src/interface/validations/static.spec.js +29 -1
  15. package/src/interface/webhook.js +16 -4
  16. package/src/interface/workflow.js +32 -54
  17. package/src/internal_activities/index.js +16 -12
  18. package/src/tracing/index.d.ts +47 -0
  19. package/src/tracing/index.js +154 -0
  20. package/src/tracing/index.private.spec.js +84 -0
  21. package/src/tracing/index.public.spec.js +86 -0
  22. package/src/tracing/tracer_tree.js +83 -0
  23. package/src/tracing/tracer_tree.spec.js +115 -0
  24. package/src/tracing/utils.js +21 -0
  25. package/src/tracing/utils.spec.js +14 -0
  26. package/src/worker/catalog_workflow/catalog.js +19 -10
  27. package/src/worker/index.js +1 -5
  28. package/src/worker/interceptors/activity.js +28 -10
  29. package/src/worker/interceptors/workflow.js +19 -1
  30. package/src/worker/loader.js +6 -6
  31. package/src/worker/loader.spec.js +6 -9
  32. package/src/worker/sinks.js +56 -10
  33. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +35 -4
  34. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +12 -4
  35. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -4
  36. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +13 -4
  37. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +16 -2
  38. package/src/worker/webpack_loaders/workflow_rewriter/tools.js +46 -13
  39. package/src/worker/webpack_loaders/workflow_rewriter/tools.spec.js +20 -2
  40. package/src/worker/tracer/index.js +0 -75
  41. package/src/worker/tracer/index.test.js +0 -103
  42. package/src/worker/tracer/tracer_tree.js +0 -84
  43. package/src/worker/tracer/tracer_tree.test.js +0 -115
  44. /package/src/{worker/async_storage.js → async_storage.js} +0 -0
  45. /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 ( stepImports.length + flowImports.length === 0 ) {
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 {import('@babel/types').File} ast - Parsed file AST.
19
- * @param {Array<{localName:string,stepName:string}>} stepImports - Step imports.
20
- * @param {Array<{localName:string,workflowName:string}>} flowImports - Workflow imports.
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 from exported step variable name to declared step name.
169
- * Parses the steps module and extracts `step({ name: '...' })` names.
170
- * @param {string} path - Absolute path to the steps module file.
171
- * @param {Map<string, Map<string,string>>} cache - Cache of computed step name maps.
172
- * @returns {Map<string,string>} Exported identifier -> step name.
173
- * @throws {Error} When a step name is invalid (non-static or missing).
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
- export const buildStepsNameMap = ( path, cache ) => {
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 stepMap = ast.program.body
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: 'step' } ) )
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, `Invalid step name in ${path} for "${dec.id.name}"` )
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, stepMap );
198
- return stepMap;
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-flow-' ) );
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
-