@outputai/core 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/LICENSE +201 -0
- package/README.md +11 -0
- package/bin/healthcheck.mjs +36 -0
- package/bin/healthcheck.spec.js +90 -0
- package/bin/worker.sh +26 -0
- package/package.json +67 -0
- package/src/activity_integration/context.d.ts +27 -0
- package/src/activity_integration/context.js +17 -0
- package/src/activity_integration/context.spec.js +42 -0
- package/src/activity_integration/events.d.ts +7 -0
- package/src/activity_integration/events.js +10 -0
- package/src/activity_integration/index.d.ts +9 -0
- package/src/activity_integration/index.js +3 -0
- package/src/activity_integration/tracing.d.ts +32 -0
- package/src/activity_integration/tracing.js +37 -0
- package/src/async_storage.js +19 -0
- package/src/bus.js +3 -0
- package/src/consts.js +32 -0
- package/src/errors.d.ts +15 -0
- package/src/errors.js +14 -0
- package/src/hooks/index.d.ts +28 -0
- package/src/hooks/index.js +32 -0
- package/src/index.d.ts +49 -0
- package/src/index.js +4 -0
- package/src/interface/evaluation_result.d.ts +173 -0
- package/src/interface/evaluation_result.js +215 -0
- package/src/interface/evaluator.d.ts +70 -0
- package/src/interface/evaluator.js +34 -0
- package/src/interface/evaluator.spec.js +565 -0
- package/src/interface/index.d.ts +9 -0
- package/src/interface/index.js +26 -0
- package/src/interface/step.d.ts +138 -0
- package/src/interface/step.js +22 -0
- package/src/interface/types.d.ts +27 -0
- package/src/interface/validations/runtime.js +20 -0
- package/src/interface/validations/runtime.spec.js +29 -0
- package/src/interface/validations/schema_utils.js +8 -0
- package/src/interface/validations/schema_utils.spec.js +67 -0
- package/src/interface/validations/static.js +136 -0
- package/src/interface/validations/static.spec.js +366 -0
- package/src/interface/webhook.d.ts +84 -0
- package/src/interface/webhook.js +64 -0
- package/src/interface/webhook.spec.js +122 -0
- package/src/interface/workflow.d.ts +273 -0
- package/src/interface/workflow.js +128 -0
- package/src/interface/workflow.spec.js +467 -0
- package/src/interface/workflow_context.js +31 -0
- package/src/interface/workflow_utils.d.ts +76 -0
- package/src/interface/workflow_utils.js +50 -0
- package/src/interface/workflow_utils.spec.js +190 -0
- package/src/interface/zod_integration.spec.js +646 -0
- package/src/internal_activities/index.js +66 -0
- package/src/internal_activities/index.spec.js +102 -0
- package/src/logger.js +73 -0
- package/src/tracing/internal_interface.js +71 -0
- package/src/tracing/processors/local/index.js +111 -0
- package/src/tracing/processors/local/index.spec.js +149 -0
- package/src/tracing/processors/s3/configs.js +31 -0
- package/src/tracing/processors/s3/configs.spec.js +64 -0
- package/src/tracing/processors/s3/index.js +114 -0
- package/src/tracing/processors/s3/index.spec.js +153 -0
- package/src/tracing/processors/s3/redis_client.js +62 -0
- package/src/tracing/processors/s3/redis_client.spec.js +185 -0
- package/src/tracing/processors/s3/s3_client.js +27 -0
- package/src/tracing/processors/s3/s3_client.spec.js +62 -0
- package/src/tracing/tools/build_trace_tree.js +83 -0
- package/src/tracing/tools/build_trace_tree.spec.js +135 -0
- package/src/tracing/tools/utils.js +21 -0
- package/src/tracing/tools/utils.spec.js +14 -0
- package/src/tracing/trace_engine.js +97 -0
- package/src/tracing/trace_engine.spec.js +199 -0
- package/src/utils/index.d.ts +134 -0
- package/src/utils/index.js +2 -0
- package/src/utils/resolve_invocation_dir.js +34 -0
- package/src/utils/resolve_invocation_dir.spec.js +102 -0
- package/src/utils/utils.js +211 -0
- package/src/utils/utils.spec.js +448 -0
- package/src/worker/bundler_options.js +43 -0
- package/src/worker/catalog_workflow/catalog.js +114 -0
- package/src/worker/catalog_workflow/index.js +54 -0
- package/src/worker/catalog_workflow/index.spec.js +196 -0
- package/src/worker/catalog_workflow/workflow.js +24 -0
- package/src/worker/configs.js +49 -0
- package/src/worker/configs.spec.js +130 -0
- package/src/worker/index.js +89 -0
- package/src/worker/index.spec.js +177 -0
- package/src/worker/interceptors/activity.js +62 -0
- package/src/worker/interceptors/activity.spec.js +212 -0
- package/src/worker/interceptors/workflow.js +70 -0
- package/src/worker/interceptors/workflow.spec.js +167 -0
- package/src/worker/interceptors.js +10 -0
- package/src/worker/loader.js +151 -0
- package/src/worker/loader.spec.js +236 -0
- package/src/worker/loader_tools.js +132 -0
- package/src/worker/loader_tools.spec.js +156 -0
- package/src/worker/log_hooks.js +95 -0
- package/src/worker/log_hooks.spec.js +217 -0
- package/src/worker/sandboxed_utils.js +18 -0
- package/src/worker/shutdown.js +26 -0
- package/src/worker/shutdown.spec.js +82 -0
- package/src/worker/sinks.js +74 -0
- package/src/worker/start_catalog.js +36 -0
- package/src/worker/start_catalog.spec.js +118 -0
- package/src/worker/webpack_loaders/consts.js +9 -0
- package/src/worker/webpack_loaders/tools.js +548 -0
- package/src/worker/webpack_loaders/tools.spec.js +330 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +613 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import buildTraceTree from './build_trace_tree.js';
|
|
3
|
+
|
|
4
|
+
describe( 'build_trace_tree', () => {
|
|
5
|
+
it( 'returns null when entries is empty', () => {
|
|
6
|
+
expect( buildTraceTree( [] ) ).toBeNull();
|
|
7
|
+
} );
|
|
8
|
+
|
|
9
|
+
it( 'sets root output with a fixed message when workflow has no end/error phase yet', () => {
|
|
10
|
+
const entries = [
|
|
11
|
+
{ kind: 'workflow', id: 'wf', parentId: undefined, phase: 'start', name: 'wf', details: {}, timestamp: 1000 }
|
|
12
|
+
];
|
|
13
|
+
const result = buildTraceTree( entries );
|
|
14
|
+
expect( result ).not.toBeNull();
|
|
15
|
+
expect( result.output ).toBe( '<<Workflow did not finish yet. If this workflows is supposed to have been completed already, \
|
|
16
|
+
this can indicate it timed out or was interrupted.>>' );
|
|
17
|
+
expect( result.endedAt ).toBeNull();
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'returns null when there is no root (all entries have parentId)', () => {
|
|
21
|
+
const entries = [
|
|
22
|
+
{ id: 'a', parentId: 'x', phase: 'start', name: 'a', timestamp: 1 },
|
|
23
|
+
{ id: 'b', parentId: 'a', phase: 'start', name: 'b', timestamp: 2 }
|
|
24
|
+
];
|
|
25
|
+
expect( buildTraceTree( entries ) ).toBeNull();
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'error phase sets error and endedAt on node', () => {
|
|
29
|
+
const entries = [
|
|
30
|
+
{ kind: 'wf', id: 'r', parentId: undefined, phase: 'start', name: 'root', details: {}, timestamp: 100 },
|
|
31
|
+
{ kind: 'step', id: 's', parentId: 'r', phase: 'start', name: 'step', details: {}, timestamp: 200 },
|
|
32
|
+
{ id: 's', phase: 'error', details: { message: 'failed' }, timestamp: 300 }
|
|
33
|
+
];
|
|
34
|
+
const result = buildTraceTree( entries );
|
|
35
|
+
expect( result ).not.toBeNull();
|
|
36
|
+
expect( result.children ).toHaveLength( 1 );
|
|
37
|
+
expect( result.children[0].error ).toEqual( { message: 'failed' } );
|
|
38
|
+
expect( result.children[0].endedAt ).toBe( 300 );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'builds a tree from workflow/step/IO entries with grouping and sorting', () => {
|
|
42
|
+
const entries = [
|
|
43
|
+
// workflow start
|
|
44
|
+
{ kind: 'workflow', phase: 'start', name: 'wf', id: 'wf', parentId: undefined, details: { a: 1 }, timestamp: 1000 },
|
|
45
|
+
// evaluator start/stop
|
|
46
|
+
{ kind: 'evaluator', phase: 'start', name: 'eval', id: 'eval', parentId: 'wf', details: { z: 0 }, timestamp: 1500 },
|
|
47
|
+
{ id: 'eval', phase: 'end', details: { z: 1 }, timestamp: 1600 },
|
|
48
|
+
// step1 start
|
|
49
|
+
{ kind: 'step', phase: 'start', name: 'step-1', id: 's1', parentId: 'wf', details: { x: 1 }, timestamp: 2000 },
|
|
50
|
+
// IO under step1
|
|
51
|
+
{ kind: 'IO', phase: 'start', name: 'test-1', id: 'io1', parentId: 's1', details: { y: 2 }, timestamp: 2300 },
|
|
52
|
+
// step2 start
|
|
53
|
+
{ kind: 'step', phase: 'start', name: 'step-2', id: 's2', parentId: 'wf', details: { x: 2 }, timestamp: 2400 },
|
|
54
|
+
// IO under step2
|
|
55
|
+
{ kind: 'IO', phase: 'start', name: 'test-2', id: 'io2', parentId: 's2', details: { y: 3 }, timestamp: 2500 },
|
|
56
|
+
{ id: 'io2', phase: 'end', details: { y: 4 }, timestamp: 2600 },
|
|
57
|
+
// IO under step1 ends
|
|
58
|
+
{ id: 'io1', phase: 'end', details: { y: 5 }, timestamp: 2700 },
|
|
59
|
+
// step1 end
|
|
60
|
+
{ id: 's1', phase: 'end', details: { done: true }, timestamp: 2800 },
|
|
61
|
+
// step2 end
|
|
62
|
+
{ id: 's2', phase: 'end', details: { done: true }, timestamp: 2900 },
|
|
63
|
+
// workflow end
|
|
64
|
+
{ id: 'wf', phase: 'end', details: { ok: true }, timestamp: 3000 }
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const result = buildTraceTree( entries );
|
|
68
|
+
|
|
69
|
+
const expected = {
|
|
70
|
+
id: 'wf',
|
|
71
|
+
kind: 'workflow',
|
|
72
|
+
name: 'wf',
|
|
73
|
+
startedAt: 1000,
|
|
74
|
+
endedAt: 3000,
|
|
75
|
+
input: { a: 1 },
|
|
76
|
+
output: { ok: true },
|
|
77
|
+
children: [
|
|
78
|
+
{
|
|
79
|
+
id: 'eval',
|
|
80
|
+
kind: 'evaluator',
|
|
81
|
+
name: 'eval',
|
|
82
|
+
startedAt: 1500,
|
|
83
|
+
endedAt: 1600,
|
|
84
|
+
input: { z: 0 },
|
|
85
|
+
output: { z: 1 },
|
|
86
|
+
children: []
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 's1',
|
|
90
|
+
kind: 'step',
|
|
91
|
+
name: 'step-1',
|
|
92
|
+
startedAt: 2000,
|
|
93
|
+
endedAt: 2800,
|
|
94
|
+
input: { x: 1 },
|
|
95
|
+
output: { done: true },
|
|
96
|
+
children: [
|
|
97
|
+
{
|
|
98
|
+
id: 'io1',
|
|
99
|
+
kind: 'IO',
|
|
100
|
+
name: 'test-1',
|
|
101
|
+
startedAt: 2300,
|
|
102
|
+
endedAt: 2700,
|
|
103
|
+
input: { y: 2 },
|
|
104
|
+
output: { y: 5 },
|
|
105
|
+
children: []
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 's2',
|
|
111
|
+
kind: 'step',
|
|
112
|
+
name: 'step-2',
|
|
113
|
+
startedAt: 2400,
|
|
114
|
+
endedAt: 2900,
|
|
115
|
+
input: { x: 2 },
|
|
116
|
+
output: { done: true },
|
|
117
|
+
children: [
|
|
118
|
+
{
|
|
119
|
+
id: 'io2',
|
|
120
|
+
kind: 'IO',
|
|
121
|
+
name: 'test-2',
|
|
122
|
+
startedAt: 2500,
|
|
123
|
+
endedAt: 2600,
|
|
124
|
+
input: { y: 3 },
|
|
125
|
+
output: { y: 4 },
|
|
126
|
+
children: []
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
expect( result ).toMatchObject( expected );
|
|
134
|
+
} );
|
|
135
|
+
} );
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} SerializedError
|
|
3
|
+
* @property {string} name - The error constructor name
|
|
4
|
+
* @property {string} message - The error message
|
|
5
|
+
* @property {string} stack - The error stack trace
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serialize an error object.
|
|
10
|
+
*
|
|
11
|
+
* If it has ".cause", recursive serialize its cause until finally found an error without it.
|
|
12
|
+
*
|
|
13
|
+
* @param {Error} error
|
|
14
|
+
* @returns {SerializedError}
|
|
15
|
+
*/
|
|
16
|
+
export const serializeError = error =>
|
|
17
|
+
error.cause ? serializeError( error.cause ) : {
|
|
18
|
+
name: error.constructor.name,
|
|
19
|
+
message: error.message,
|
|
20
|
+
stack: error.stack
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { serializeError } from './utils.js';
|
|
3
|
+
|
|
4
|
+
describe( 'tracing/utils', () => {
|
|
5
|
+
it( 'serializeError unwraps causes and keeps message/stack', () => {
|
|
6
|
+
const inner = new Error( 'inner' );
|
|
7
|
+
const outer = new Error( 'outer', { cause: inner } );
|
|
8
|
+
|
|
9
|
+
const out = serializeError( outer );
|
|
10
|
+
expect( out.name ).toBe( 'Error' );
|
|
11
|
+
expect( out.message ).toBe( 'inner' );
|
|
12
|
+
expect( typeof out.stack ).toBe( 'string' );
|
|
13
|
+
} );
|
|
14
|
+
} );
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Storage } from '#async_storage';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { serializeError } from './tools/utils.js';
|
|
4
|
+
import { isStringboolTrue } from '#utils';
|
|
5
|
+
import * as localProcessor from './processors/local/index.js';
|
|
6
|
+
import * as s3Processor from './processors/s3/index.js';
|
|
7
|
+
import { ComponentType } from '#consts';
|
|
8
|
+
import { createChildLogger } from '#logger';
|
|
9
|
+
|
|
10
|
+
const log = createChildLogger( 'Tracing' );
|
|
11
|
+
|
|
12
|
+
const traceBus = new EventEmitter();
|
|
13
|
+
const processors = [
|
|
14
|
+
{
|
|
15
|
+
enabled: isStringboolTrue( process.env.OUTPUT_TRACE_LOCAL_ON ),
|
|
16
|
+
name: 'LOCAL',
|
|
17
|
+
init: localProcessor.init,
|
|
18
|
+
exec: localProcessor.exec,
|
|
19
|
+
getDestination: localProcessor.getDestination
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
enabled: isStringboolTrue( process.env.OUTPUT_TRACE_REMOTE_ON ),
|
|
23
|
+
name: 'REMOTE',
|
|
24
|
+
init: s3Processor.init,
|
|
25
|
+
exec: s3Processor.exec,
|
|
26
|
+
getDestination: s3Processor.getDestination
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the destinations for a given execution context
|
|
32
|
+
*
|
|
33
|
+
* @param {object} executionContext
|
|
34
|
+
* @param {string} executionContext.startTime
|
|
35
|
+
* @param {string} executionContext.workflowId
|
|
36
|
+
* @param {string} executionContext.workflowName
|
|
37
|
+
* @param {boolean} executionContext.disableTrace
|
|
38
|
+
* @returns {object} A trace destinations object: { [dest-name]: 'path' }
|
|
39
|
+
*/
|
|
40
|
+
export const getDestinations = executionContext =>
|
|
41
|
+
processors.reduce( ( o, p ) =>
|
|
42
|
+
Object.assign( o, { [p.name.toLowerCase()]: p.enabled && !executionContext.disableTrace ? p.getDestination( executionContext ) : null } )
|
|
43
|
+
, {} );
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Starts processors based on env vars and attach them to the main bus to listen trace events
|
|
47
|
+
*/
|
|
48
|
+
export const init = async () => {
|
|
49
|
+
for ( const p of processors.filter( p => p.enabled ) ) {
|
|
50
|
+
await p.init();
|
|
51
|
+
traceBus.addListener( 'entry', async ( ...args ) => {
|
|
52
|
+
try {
|
|
53
|
+
await p.exec( ...args );
|
|
54
|
+
} catch ( error ) {
|
|
55
|
+
log.error( 'Processor execution error', { processor: p.name, error: error.message, stack: error.stack } );
|
|
56
|
+
}
|
|
57
|
+
} );
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Serialize details of an event
|
|
63
|
+
*/
|
|
64
|
+
const serializeDetails = details => details instanceof Error ? serializeError( details ) : details;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a new trace event phase and sends it to be written
|
|
68
|
+
*
|
|
69
|
+
* @param {string} phase - The phase
|
|
70
|
+
* @param {object} fields - All the trace fields
|
|
71
|
+
* @returns {void}
|
|
72
|
+
*/
|
|
73
|
+
export const addEventPhase = ( phase, { kind, name, id, parentId, details, executionContext } ) => {
|
|
74
|
+
// Ignores internal steps in the actual trace files, ignore trace if the flag is true
|
|
75
|
+
if ( kind !== ComponentType.INTERNAL_STEP && !executionContext.disableTrace ) {
|
|
76
|
+
traceBus.emit( 'entry', {
|
|
77
|
+
executionContext,
|
|
78
|
+
entry: { kind, phase, name, id, parentId, phase, timestamp: Date.now(), details: serializeDetails( details ) }
|
|
79
|
+
} );
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Adds an Event Phase, complementing the options with parentId and executionContext from the async storage.
|
|
85
|
+
*
|
|
86
|
+
* This function will have no effect if called from outside an Temporal Workflow/Activity environment,
|
|
87
|
+
* so it is safe to be used on unit tests or any dependencies that might be used elsewhere
|
|
88
|
+
*
|
|
89
|
+
* @param {object} options - The common trace configurations
|
|
90
|
+
*/
|
|
91
|
+
export function addEventPhaseWithContext( phase, options ) {
|
|
92
|
+
const storeContent = Storage.load();
|
|
93
|
+
if ( storeContent ) { // If there is no storageContext this was not called from an Temporal Environment
|
|
94
|
+
const { parentId, executionContext } = storeContent;
|
|
95
|
+
addEventPhase( phase, { ...options, parentId, executionContext } );
|
|
96
|
+
}
|
|
97
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const storageLoadMock = vi.fn();
|
|
4
|
+
vi.mock( '#async_storage', () => ( {
|
|
5
|
+
Storage: { load: storageLoadMock }
|
|
6
|
+
} ) );
|
|
7
|
+
|
|
8
|
+
const localInitMock = vi.fn( async () => {} );
|
|
9
|
+
const localExecMock = vi.fn();
|
|
10
|
+
const localGetDestinationMock = vi.fn( () => '/local/path.json' );
|
|
11
|
+
vi.mock( './processors/local/index.js', () => ( {
|
|
12
|
+
init: localInitMock,
|
|
13
|
+
exec: localExecMock,
|
|
14
|
+
getDestination: localGetDestinationMock
|
|
15
|
+
} ) );
|
|
16
|
+
|
|
17
|
+
const s3InitMock = vi.fn( async () => {} );
|
|
18
|
+
const s3ExecMock = vi.fn();
|
|
19
|
+
const s3GetDestinationMock = vi.fn( () => 'https://bucket.s3.amazonaws.com/key.json' );
|
|
20
|
+
vi.mock( './processors/s3/index.js', () => ( {
|
|
21
|
+
init: s3InitMock,
|
|
22
|
+
exec: s3ExecMock,
|
|
23
|
+
getDestination: s3GetDestinationMock
|
|
24
|
+
} ) );
|
|
25
|
+
|
|
26
|
+
async function loadTraceEngine() {
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
return import( './trace_engine.js' );
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe( 'tracing/trace_engine', () => {
|
|
32
|
+
beforeEach( () => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
delete process.env.OUTPUT_TRACE_LOCAL_ON;
|
|
35
|
+
delete process.env.OUTPUT_TRACE_REMOTE_ON;
|
|
36
|
+
storageLoadMock.mockReset();
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'init() starts only enabled processors and attaches listeners', async () => {
|
|
40
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
41
|
+
process.env.OUTPUT_TRACE_REMOTE_ON = '0';
|
|
42
|
+
const { init, addEventPhase } = await loadTraceEngine();
|
|
43
|
+
|
|
44
|
+
await init();
|
|
45
|
+
|
|
46
|
+
expect( localInitMock ).toHaveBeenCalledTimes( 1 );
|
|
47
|
+
expect( s3InitMock ).not.toHaveBeenCalled();
|
|
48
|
+
|
|
49
|
+
const executionContext = { disableTrace: false };
|
|
50
|
+
addEventPhase( 'start', {
|
|
51
|
+
kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true }, executionContext
|
|
52
|
+
} );
|
|
53
|
+
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
54
|
+
const payload = localExecMock.mock.calls[0][0];
|
|
55
|
+
expect( payload.entry.name ).toBe( 'N' );
|
|
56
|
+
expect( payload.entry.kind ).toBe( 'step' );
|
|
57
|
+
expect( payload.entry.phase ).toBe( 'start' );
|
|
58
|
+
expect( payload.entry.details ).toEqual( { ok: true } );
|
|
59
|
+
expect( payload.executionContext ).toBe( executionContext );
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
it( 'addEventPhase() emits an entry consumed by processors', async () => {
|
|
63
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = 'on';
|
|
64
|
+
const { init, addEventPhase } = await loadTraceEngine();
|
|
65
|
+
await init();
|
|
66
|
+
|
|
67
|
+
addEventPhase( 'end', {
|
|
68
|
+
kind: 'workflow', name: 'W', id: '2', parentId: 'p2', details: 'done',
|
|
69
|
+
executionContext: { disableTrace: false }
|
|
70
|
+
} );
|
|
71
|
+
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
72
|
+
const payload = localExecMock.mock.calls[0][0];
|
|
73
|
+
expect( payload.entry.name ).toBe( 'W' );
|
|
74
|
+
expect( payload.entry.phase ).toBe( 'end' );
|
|
75
|
+
expect( payload.entry.details ).toBe( 'done' );
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
it( 'addEventPhase() does not emit when executionContext.disableTrace is true', async () => {
|
|
79
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
80
|
+
const { init, addEventPhase } = await loadTraceEngine();
|
|
81
|
+
await init();
|
|
82
|
+
|
|
83
|
+
addEventPhase( 'start', {
|
|
84
|
+
kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
|
|
85
|
+
executionContext: { disableTrace: true }
|
|
86
|
+
} );
|
|
87
|
+
expect( localExecMock ).not.toHaveBeenCalled();
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
it( 'addEventPhase() does not emit when kind is INTERNAL_STEP', async () => {
|
|
91
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
92
|
+
const { init, addEventPhase } = await loadTraceEngine();
|
|
93
|
+
await init();
|
|
94
|
+
|
|
95
|
+
addEventPhase( 'start', {
|
|
96
|
+
kind: 'internal_step', name: 'Internal', id: '1', parentId: 'p', details: {},
|
|
97
|
+
executionContext: { disableTrace: false }
|
|
98
|
+
} );
|
|
99
|
+
expect( localExecMock ).not.toHaveBeenCalled();
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'addEventPhaseWithContext() uses storage when available', async () => {
|
|
103
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
|
|
104
|
+
storageLoadMock.mockReturnValue( {
|
|
105
|
+
parentId: 'ctx-p',
|
|
106
|
+
executionContext: { runId: 'r1', disableTrace: false }
|
|
107
|
+
} );
|
|
108
|
+
const { init, addEventPhaseWithContext } = await loadTraceEngine();
|
|
109
|
+
await init();
|
|
110
|
+
|
|
111
|
+
addEventPhaseWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
|
|
112
|
+
expect( localExecMock ).toHaveBeenCalledTimes( 1 );
|
|
113
|
+
const payload = localExecMock.mock.calls[0][0];
|
|
114
|
+
expect( payload.executionContext ).toEqual( { runId: 'r1', disableTrace: false } );
|
|
115
|
+
expect( payload.entry.parentId ).toBe( 'ctx-p' );
|
|
116
|
+
expect( payload.entry.name ).toBe( 'S' );
|
|
117
|
+
expect( payload.entry.phase ).toBe( 'tick' );
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
it( 'addEventPhaseWithContext() does not emit when storage executionContext.disableTrace is true', async () => {
|
|
121
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
122
|
+
storageLoadMock.mockReturnValue( {
|
|
123
|
+
parentId: 'ctx-p',
|
|
124
|
+
executionContext: { runId: 'r1', disableTrace: true }
|
|
125
|
+
} );
|
|
126
|
+
const { init, addEventPhaseWithContext } = await loadTraceEngine();
|
|
127
|
+
await init();
|
|
128
|
+
|
|
129
|
+
addEventPhaseWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
|
|
130
|
+
expect( localExecMock ).not.toHaveBeenCalled();
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
it( 'addEventPhaseWithContext() is a no-op when storage is absent', async () => {
|
|
134
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
135
|
+
storageLoadMock.mockReturnValue( undefined );
|
|
136
|
+
const { init, addEventPhaseWithContext } = await loadTraceEngine();
|
|
137
|
+
await init();
|
|
138
|
+
|
|
139
|
+
addEventPhaseWithContext( 'noop', { kind: 'step', name: 'X', id: '4', details: null } );
|
|
140
|
+
expect( localExecMock ).not.toHaveBeenCalled();
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
describe( 'getDestinations()', () => {
|
|
144
|
+
const executionContext = { workflowId: 'w1', workflowName: 'WF', startTime: 1, disableTrace: false };
|
|
145
|
+
|
|
146
|
+
it( 'returns null for both when traces are off (env vars unset)', async () => {
|
|
147
|
+
const { getDestinations } = await loadTraceEngine();
|
|
148
|
+
const result = getDestinations( executionContext );
|
|
149
|
+
expect( result ).toEqual( { local: null, remote: null } );
|
|
150
|
+
expect( localGetDestinationMock ).not.toHaveBeenCalled();
|
|
151
|
+
expect( s3GetDestinationMock ).not.toHaveBeenCalled();
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
it( 'returns null for both when executionContext.disableTrace is true', async () => {
|
|
155
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
156
|
+
process.env.OUTPUT_TRACE_REMOTE_ON = '1';
|
|
157
|
+
const { getDestinations } = await loadTraceEngine();
|
|
158
|
+
const result = getDestinations( { ...executionContext, disableTrace: true } );
|
|
159
|
+
expect( result ).toEqual( { local: null, remote: null } );
|
|
160
|
+
expect( localGetDestinationMock ).not.toHaveBeenCalled();
|
|
161
|
+
expect( s3GetDestinationMock ).not.toHaveBeenCalled();
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
it( 'returns both destinations when both traces are on', async () => {
|
|
165
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
166
|
+
process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
|
|
167
|
+
const { getDestinations } = await loadTraceEngine();
|
|
168
|
+
const result = getDestinations( executionContext );
|
|
169
|
+
expect( result ).toEqual( {
|
|
170
|
+
local: '/local/path.json',
|
|
171
|
+
remote: 'https://bucket.s3.amazonaws.com/key.json'
|
|
172
|
+
} );
|
|
173
|
+
expect( localGetDestinationMock ).toHaveBeenCalledTimes( 1 );
|
|
174
|
+
expect( localGetDestinationMock ).toHaveBeenCalledWith( executionContext );
|
|
175
|
+
expect( s3GetDestinationMock ).toHaveBeenCalledTimes( 1 );
|
|
176
|
+
expect( s3GetDestinationMock ).toHaveBeenCalledWith( executionContext );
|
|
177
|
+
} );
|
|
178
|
+
|
|
179
|
+
it( 'returns local only when local trace on and remote off', async () => {
|
|
180
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '1';
|
|
181
|
+
process.env.OUTPUT_TRACE_REMOTE_ON = '0';
|
|
182
|
+
const { getDestinations } = await loadTraceEngine();
|
|
183
|
+
const result = getDestinations( executionContext );
|
|
184
|
+
expect( result ).toEqual( { local: '/local/path.json', remote: null } );
|
|
185
|
+
expect( localGetDestinationMock ).toHaveBeenCalledWith( executionContext );
|
|
186
|
+
expect( s3GetDestinationMock ).not.toHaveBeenCalled();
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
it( 'returns remote only when local trace off and remote on', async () => {
|
|
190
|
+
process.env.OUTPUT_TRACE_LOCAL_ON = '0';
|
|
191
|
+
process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
|
|
192
|
+
const { getDestinations } = await loadTraceEngine();
|
|
193
|
+
const result = getDestinations( executionContext );
|
|
194
|
+
expect( result ).toEqual( { local: null, remote: 'https://bucket.s3.amazonaws.com/key.json' } );
|
|
195
|
+
expect( localGetDestinationMock ).not.toHaveBeenCalled();
|
|
196
|
+
expect( s3GetDestinationMock ).toHaveBeenCalledWith( executionContext );
|
|
197
|
+
} );
|
|
198
|
+
} );
|
|
199
|
+
} );
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* > [!WARNING]
|
|
3
|
+
* > **Internal use only.** Not part of the public API; may change without notice.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Return the first immediate directory of the file invoking the code that called this function.
|
|
10
|
+
*
|
|
11
|
+
* Excludes `@outputai/core`, node, and other internal paths.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveInvocationDir(): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Node safe clone implementation that doesn't use global structuredClone().
|
|
17
|
+
*
|
|
18
|
+
* Returns a cloned version of the object.
|
|
19
|
+
*
|
|
20
|
+
* Only clones static properties. Getters become static properties.
|
|
21
|
+
*
|
|
22
|
+
* @param object
|
|
23
|
+
*/
|
|
24
|
+
export function clone( object: object ): object;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Receives an error as argument and throws it.
|
|
28
|
+
*
|
|
29
|
+
* @param error
|
|
30
|
+
* @throws {Error}
|
|
31
|
+
*/
|
|
32
|
+
export function throws( error: Error ): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Attach given value to an object with the METADATA_ACCESS_SYMBOL symbol as key.
|
|
36
|
+
*
|
|
37
|
+
* @param target
|
|
38
|
+
* @param value
|
|
39
|
+
* @returns
|
|
40
|
+
*/
|
|
41
|
+
export function setMetadata( target: object, value: object ): void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read metadata previously attached via setMetadata.
|
|
45
|
+
*
|
|
46
|
+
* @param target - The function or object to read metadata from.
|
|
47
|
+
* @returns The metadata object, or null if none is attached.
|
|
48
|
+
*/
|
|
49
|
+
export function getMetadata( target: Function ): { name: string; description?: string; type?: string } | null;
|
|
50
|
+
|
|
51
|
+
/** Represents a {Response} serialized to plain object */
|
|
52
|
+
export type SerializedFetchResponse = {
|
|
53
|
+
/** The response url */
|
|
54
|
+
url: string,
|
|
55
|
+
|
|
56
|
+
/** The response status code */
|
|
57
|
+
status: number,
|
|
58
|
+
|
|
59
|
+
/** The response status text */
|
|
60
|
+
statusText: string,
|
|
61
|
+
|
|
62
|
+
/** Flag indicating if the request succeeded */
|
|
63
|
+
ok: boolean,
|
|
64
|
+
|
|
65
|
+
/** Object with response headers */
|
|
66
|
+
headers: Record<string, string>,
|
|
67
|
+
|
|
68
|
+
/** Response body, either JSON, text or arrayBuffer converter to base64 */
|
|
69
|
+
body: object | string
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Consumes an HTTP `Response` and serializes it to a plain object.
|
|
74
|
+
*
|
|
75
|
+
* @param response - The response to serialize.
|
|
76
|
+
* @returns SerializedFetchResponse
|
|
77
|
+
*/
|
|
78
|
+
export function serializeFetchResponse( response: Response ): SerializedFetchResponse;
|
|
79
|
+
|
|
80
|
+
export type SerializedBodyAndContentType = {
|
|
81
|
+
/** The body as a string when possible; otherwise the original value */
|
|
82
|
+
body: string | unknown,
|
|
83
|
+
/** The inferred `Content-Type` header value, if any */
|
|
84
|
+
contentType: string | undefined
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Serializes a payload for use as a fetch POST body and infers its `Content-Type`.
|
|
89
|
+
*
|
|
90
|
+
* @param body - The payload to serialize.
|
|
91
|
+
* @returns The serialized body and inferred `Content-Type`.
|
|
92
|
+
*/
|
|
93
|
+
export function serializeBodyAndInferContentType( body: unknown ): SerializedBodyAndContentType;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns true if the value is a plain object:
|
|
97
|
+
* - `{}`
|
|
98
|
+
* - `new Object()`
|
|
99
|
+
* - `Object.create(null)`
|
|
100
|
+
*
|
|
101
|
+
* @param object - The value to check.
|
|
102
|
+
* @returns Whether the value is a plain object.
|
|
103
|
+
*/
|
|
104
|
+
export function isPlainObject( object: unknown ): boolean;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns a copy of an array with its content shuffled.
|
|
108
|
+
*
|
|
109
|
+
* @param arr - The array to shuffle
|
|
110
|
+
* @returns A shuffled array copy
|
|
111
|
+
*/
|
|
112
|
+
export function shuffleArray( arr: unknown[] ): unknown[];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates a new object by merging object `b` onto object `a`, biased toward `b`:
|
|
116
|
+
* - Fields in `b` overwrite fields in `a`.
|
|
117
|
+
* - Fields in `b` that don't exist in `a` are created.
|
|
118
|
+
* - Fields in `a` that don't exist in `b` are left unchanged.
|
|
119
|
+
*
|
|
120
|
+
* @param a - The base object.
|
|
121
|
+
* @param b - The overriding object.
|
|
122
|
+
* @throws {Error} If either `a` or `b` is not a plain object.
|
|
123
|
+
* @returns A new merged object.
|
|
124
|
+
*/
|
|
125
|
+
export function deepMerge( a: object, b: object ): object;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Shortens a UUID to a url-safe base64-like string (custom 64-char alphabet).
|
|
129
|
+
* Temporal-friendly: no Buffer or crypto; safe to use inside workflows.
|
|
130
|
+
*
|
|
131
|
+
* @param uuid - Standard UUID (e.g. `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).
|
|
132
|
+
* @returns Short string using A–Z, a–z, 0–9, `_`, `-` (typically 21–22 chars).
|
|
133
|
+
*/
|
|
134
|
+
export function toUrlSafeBase64( uuid: string ): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as stackTraceParser from 'stacktrace-parser';
|
|
2
|
+
|
|
3
|
+
// OS separator, but in a deterministic way, allowing this to work in Temporal's sandbox
|
|
4
|
+
// This avoids importing from node:path
|
|
5
|
+
const SEP = new Error().stack.includes( '/' ) ? '/' : '\\';
|
|
6
|
+
|
|
7
|
+
const transformSeparators = path => path.replaceAll( '/', SEP );
|
|
8
|
+
const defaultIgnorePaths = [
|
|
9
|
+
'/@outputai/core/',
|
|
10
|
+
'/@outputai/llm/',
|
|
11
|
+
'/@outputai/evals/',
|
|
12
|
+
'/sdk/core/',
|
|
13
|
+
'/sdk/llm/',
|
|
14
|
+
'/sdk/evals/',
|
|
15
|
+
'node:internal/',
|
|
16
|
+
'evalmachine.',
|
|
17
|
+
'webpack/bootstrap'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return the directory of the file invoking the code that called this function
|
|
22
|
+
* Excludes some internal paths and the sdk itself
|
|
23
|
+
*/
|
|
24
|
+
export default ( additionalIgnorePaths = [] ) => {
|
|
25
|
+
const stack = new Error().stack;
|
|
26
|
+
const lines = stackTraceParser.parse( stack );
|
|
27
|
+
const ignorePaths = [ ...additionalIgnorePaths, ...defaultIgnorePaths ].map( transformSeparators );
|
|
28
|
+
|
|
29
|
+
const frame = lines.find( l => !ignorePaths.some( p => l.file.includes( p ) ) );
|
|
30
|
+
if ( !frame ) {
|
|
31
|
+
throw new Error( `Invocation dir resolution via stack trace failed. Stack: ${stack}` );
|
|
32
|
+
}
|
|
33
|
+
return frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
|
|
34
|
+
};
|