@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.
Files changed (114) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/bin/healthcheck.mjs +36 -0
  4. package/bin/healthcheck.spec.js +90 -0
  5. package/bin/worker.sh +26 -0
  6. package/package.json +67 -0
  7. package/src/activity_integration/context.d.ts +27 -0
  8. package/src/activity_integration/context.js +17 -0
  9. package/src/activity_integration/context.spec.js +42 -0
  10. package/src/activity_integration/events.d.ts +7 -0
  11. package/src/activity_integration/events.js +10 -0
  12. package/src/activity_integration/index.d.ts +9 -0
  13. package/src/activity_integration/index.js +3 -0
  14. package/src/activity_integration/tracing.d.ts +32 -0
  15. package/src/activity_integration/tracing.js +37 -0
  16. package/src/async_storage.js +19 -0
  17. package/src/bus.js +3 -0
  18. package/src/consts.js +32 -0
  19. package/src/errors.d.ts +15 -0
  20. package/src/errors.js +14 -0
  21. package/src/hooks/index.d.ts +28 -0
  22. package/src/hooks/index.js +32 -0
  23. package/src/index.d.ts +49 -0
  24. package/src/index.js +4 -0
  25. package/src/interface/evaluation_result.d.ts +173 -0
  26. package/src/interface/evaluation_result.js +215 -0
  27. package/src/interface/evaluator.d.ts +70 -0
  28. package/src/interface/evaluator.js +34 -0
  29. package/src/interface/evaluator.spec.js +565 -0
  30. package/src/interface/index.d.ts +9 -0
  31. package/src/interface/index.js +26 -0
  32. package/src/interface/step.d.ts +138 -0
  33. package/src/interface/step.js +22 -0
  34. package/src/interface/types.d.ts +27 -0
  35. package/src/interface/validations/runtime.js +20 -0
  36. package/src/interface/validations/runtime.spec.js +29 -0
  37. package/src/interface/validations/schema_utils.js +8 -0
  38. package/src/interface/validations/schema_utils.spec.js +67 -0
  39. package/src/interface/validations/static.js +136 -0
  40. package/src/interface/validations/static.spec.js +366 -0
  41. package/src/interface/webhook.d.ts +84 -0
  42. package/src/interface/webhook.js +64 -0
  43. package/src/interface/webhook.spec.js +122 -0
  44. package/src/interface/workflow.d.ts +273 -0
  45. package/src/interface/workflow.js +128 -0
  46. package/src/interface/workflow.spec.js +467 -0
  47. package/src/interface/workflow_context.js +31 -0
  48. package/src/interface/workflow_utils.d.ts +76 -0
  49. package/src/interface/workflow_utils.js +50 -0
  50. package/src/interface/workflow_utils.spec.js +190 -0
  51. package/src/interface/zod_integration.spec.js +646 -0
  52. package/src/internal_activities/index.js +66 -0
  53. package/src/internal_activities/index.spec.js +102 -0
  54. package/src/logger.js +73 -0
  55. package/src/tracing/internal_interface.js +71 -0
  56. package/src/tracing/processors/local/index.js +111 -0
  57. package/src/tracing/processors/local/index.spec.js +149 -0
  58. package/src/tracing/processors/s3/configs.js +31 -0
  59. package/src/tracing/processors/s3/configs.spec.js +64 -0
  60. package/src/tracing/processors/s3/index.js +114 -0
  61. package/src/tracing/processors/s3/index.spec.js +153 -0
  62. package/src/tracing/processors/s3/redis_client.js +62 -0
  63. package/src/tracing/processors/s3/redis_client.spec.js +185 -0
  64. package/src/tracing/processors/s3/s3_client.js +27 -0
  65. package/src/tracing/processors/s3/s3_client.spec.js +62 -0
  66. package/src/tracing/tools/build_trace_tree.js +83 -0
  67. package/src/tracing/tools/build_trace_tree.spec.js +135 -0
  68. package/src/tracing/tools/utils.js +21 -0
  69. package/src/tracing/tools/utils.spec.js +14 -0
  70. package/src/tracing/trace_engine.js +97 -0
  71. package/src/tracing/trace_engine.spec.js +199 -0
  72. package/src/utils/index.d.ts +134 -0
  73. package/src/utils/index.js +2 -0
  74. package/src/utils/resolve_invocation_dir.js +34 -0
  75. package/src/utils/resolve_invocation_dir.spec.js +102 -0
  76. package/src/utils/utils.js +211 -0
  77. package/src/utils/utils.spec.js +448 -0
  78. package/src/worker/bundler_options.js +43 -0
  79. package/src/worker/catalog_workflow/catalog.js +114 -0
  80. package/src/worker/catalog_workflow/index.js +54 -0
  81. package/src/worker/catalog_workflow/index.spec.js +196 -0
  82. package/src/worker/catalog_workflow/workflow.js +24 -0
  83. package/src/worker/configs.js +49 -0
  84. package/src/worker/configs.spec.js +130 -0
  85. package/src/worker/index.js +89 -0
  86. package/src/worker/index.spec.js +177 -0
  87. package/src/worker/interceptors/activity.js +62 -0
  88. package/src/worker/interceptors/activity.spec.js +212 -0
  89. package/src/worker/interceptors/workflow.js +70 -0
  90. package/src/worker/interceptors/workflow.spec.js +167 -0
  91. package/src/worker/interceptors.js +10 -0
  92. package/src/worker/loader.js +151 -0
  93. package/src/worker/loader.spec.js +236 -0
  94. package/src/worker/loader_tools.js +132 -0
  95. package/src/worker/loader_tools.spec.js +156 -0
  96. package/src/worker/log_hooks.js +95 -0
  97. package/src/worker/log_hooks.spec.js +217 -0
  98. package/src/worker/sandboxed_utils.js +18 -0
  99. package/src/worker/shutdown.js +26 -0
  100. package/src/worker/shutdown.spec.js +82 -0
  101. package/src/worker/sinks.js +74 -0
  102. package/src/worker/start_catalog.js +36 -0
  103. package/src/worker/start_catalog.spec.js +118 -0
  104. package/src/worker/webpack_loaders/consts.js +9 -0
  105. package/src/worker/webpack_loaders/tools.js +548 -0
  106. package/src/worker/webpack_loaders/tools.spec.js +330 -0
  107. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
  108. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
  109. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
  110. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
  111. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
  112. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
  113. package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
  114. 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,2 @@
1
+ export { default as resolveInvocationDir } from './resolve_invocation_dir.js';
2
+ export * from './utils.js';
@@ -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
+ };