@outputai/core 0.6.0 → 0.6.1-dev.daae905.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 (51) hide show
  1. package/package.json +2 -2
  2. package/src/activity_integration/context.d.ts +5 -9
  3. package/src/activity_integration/context.js +5 -4
  4. package/src/activity_integration/context.spec.js +10 -15
  5. package/src/activity_integration/events.d.ts +2 -4
  6. package/src/activity_integration/events.js +8 -3
  7. package/src/activity_integration/events.spec.js +58 -29
  8. package/src/bus.js +18 -9
  9. package/src/bus.spec.js +30 -0
  10. package/src/hooks/index.d.ts +112 -58
  11. package/src/hooks/index.js +15 -12
  12. package/src/hooks/index.spec.js +60 -32
  13. package/src/interface/workflow.js +19 -35
  14. package/src/interface/workflow.spec.js +104 -15
  15. package/src/internal_activities/index.js +3 -3
  16. package/src/internal_activities/index.spec.js +31 -1
  17. package/src/internal_utils/temporal_context.js +12 -0
  18. package/src/internal_utils/temporal_context.spec.ts +83 -0
  19. package/src/internal_utils/trace_info.js +21 -0
  20. package/src/internal_utils/trace_info.spec.js +47 -0
  21. package/src/internal_utils/workflow_context.js +29 -0
  22. package/src/internal_utils/workflow_context.spec.js +46 -0
  23. package/src/logger/development.js +61 -0
  24. package/src/logger/development.spec.js +70 -0
  25. package/src/logger/index.js +14 -0
  26. package/src/logger/index.spec.js +27 -0
  27. package/src/logger/production.js +15 -0
  28. package/src/logger/production.spec.js +52 -0
  29. package/src/tracing/internal_interface.js +4 -4
  30. package/src/tracing/processors/local/index.js +21 -26
  31. package/src/tracing/processors/local/index.spec.js +39 -45
  32. package/src/tracing/processors/s3/index.js +13 -23
  33. package/src/tracing/processors/s3/index.spec.js +33 -26
  34. package/src/tracing/trace_attribute.js +0 -1
  35. package/src/tracing/trace_engine.js +8 -12
  36. package/src/tracing/trace_engine.spec.js +31 -27
  37. package/src/worker/configs.js +2 -0
  38. package/src/worker/configs.spec.js +25 -0
  39. package/src/worker/index.js +4 -1
  40. package/src/worker/index.spec.js +4 -0
  41. package/src/worker/interceptors/activity.js +31 -29
  42. package/src/worker/interceptors/activity.spec.js +58 -26
  43. package/src/worker/interceptors/workflow.js +7 -2
  44. package/src/worker/interceptors/workflow.spec.js +42 -6
  45. package/src/worker/log_hooks.js +35 -46
  46. package/src/worker/log_hooks.spec.js +43 -46
  47. package/src/worker/setup_telemetry.js +19 -0
  48. package/src/worker/setup_telemetry.spec.js +80 -0
  49. package/src/worker/sinks.js +24 -24
  50. package/src/interface/workflow_context.js +0 -33
  51. package/src/logger.js +0 -73
@@ -25,6 +25,30 @@ import {
25
25
  onWorkflowStart
26
26
  } from './index.js';
27
27
 
28
+ const workflowDetails = {
29
+ workflowId: 'wf-1',
30
+ runId: 'run-1',
31
+ workflowType: 'myWorkflow',
32
+ firstExecutionRunId: 'run-1',
33
+ startTime: 1710000000000,
34
+ runStartTime: 1710000000000,
35
+ attempt: 1
36
+ };
37
+
38
+ const catalogWorkflowDetails = {
39
+ ...workflowDetails,
40
+ workflowType: WORKFLOW_CATALOG
41
+ };
42
+
43
+ const activityInfo = {
44
+ activityId: 'act-1',
45
+ activityType: 'wf#step',
46
+ workflowExecution: { workflowId: 'wf-1', runId: 'run-1' },
47
+ workflowType: 'myWorkflow'
48
+ };
49
+
50
+ const eventDate = 1710000001234;
51
+
28
52
  describe( 'hooks/index', () => {
29
53
  beforeEach( () => {
30
54
  vi.clearAllMocks();
@@ -43,49 +67,53 @@ describe( 'hooks/index', () => {
43
67
  expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.any( Function ) );
44
68
  } );
45
69
 
46
- it( 'invokes handler with activity-shaped payload, forwarding eventId', async () => {
70
+ it( 'invokes handler with activity-shaped payload, forwarding bus fields', async () => {
47
71
  const handler = vi.fn().mockResolvedValue( undefined );
48
72
  onError( handler );
49
73
 
50
74
  const err = new Error( 'act-fail' );
51
75
  await onHandlers[BusEventType.ACTIVITY_ERROR]( {
52
76
  eventId: 'evt-act-1',
53
- id: 'act-1',
54
- name: 'wf#step',
55
- workflowId: 'wf-run-1',
56
- workflowName: 'wf',
57
- error: err
77
+ eventDate,
78
+ activityInfo,
79
+ workflowDetails,
80
+ outputActivityKind: 'step',
81
+ error: err,
82
+ extra: 'passthrough'
58
83
  } );
59
84
 
60
85
  expect( handler ).toHaveBeenCalledWith( {
61
86
  eventId: 'evt-act-1',
87
+ eventDate,
62
88
  source: 'activity',
63
- activityId: 'act-1',
64
- activityName: 'wf#step',
65
- workflowId: 'wf-run-1',
66
- workflowName: 'wf',
67
- error: err
89
+ activityInfo,
90
+ workflowDetails,
91
+ outputActivityKind: 'step',
92
+ error: err,
93
+ extra: 'passthrough'
68
94
  } );
69
95
  } );
70
96
 
71
- it( 'invokes handler with workflow-shaped payload, forwarding eventId', async () => {
97
+ it( 'invokes handler with workflow-shaped payload, forwarding bus fields', async () => {
72
98
  const handler = vi.fn().mockResolvedValue( undefined );
73
99
  onError( handler );
74
100
 
75
101
  const err = new Error( 'wf-fail' );
76
102
  await onHandlers[BusEventType.WORKFLOW_ERROR]( {
77
103
  eventId: 'evt-wf-1',
78
- id: 'wf-run-2',
79
- name: 'myWorkflow',
80
- error: err
104
+ eventDate,
105
+ workflowDetails,
106
+ error: err,
107
+ extra: 'passthrough'
81
108
  } );
82
109
 
83
110
  expect( handler ).toHaveBeenCalledWith( {
84
111
  eventId: 'evt-wf-1',
112
+ eventDate,
85
113
  source: 'workflow',
86
- workflowId: 'wf-run-2',
87
- workflowName: 'myWorkflow',
88
- error: err
114
+ workflowDetails,
115
+ error: err,
116
+ extra: 'passthrough'
89
117
  } );
90
118
  } );
91
119
 
@@ -94,9 +122,9 @@ describe( 'hooks/index', () => {
94
122
  onError( handler );
95
123
 
96
124
  const error = new Error( 'rt' );
97
- await onHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', error } );
125
+ await onHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', eventDate, error } );
98
126
 
99
- expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', source: 'runtime', error } );
127
+ expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', eventDate, source: 'runtime', error } );
100
128
  } );
101
129
  } );
102
130
 
@@ -113,59 +141,59 @@ describe( 'hooks/index', () => {
113
141
  } );
114
142
 
115
143
  describe( 'onWorkflowStart', () => {
116
- it( 'skips catalog workflow and forwards eventId for real workflows', async () => {
144
+ it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
117
145
  const handler = vi.fn().mockResolvedValue( undefined );
118
146
  onWorkflowStart( handler );
119
147
 
120
148
  await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
121
- eventId: 'evt-ignored', id: '1', name: WORKFLOW_CATALOG
149
+ eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
122
150
  } ) );
123
151
  expect( handler ).not.toHaveBeenCalled();
124
152
 
125
153
  await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
126
- eventId: 'evt-start-1', id: '2', runId: 'run-2', name: 'myWorkflow'
154
+ eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
127
155
  } ) );
128
156
  expect( handler ).toHaveBeenCalledWith( {
129
- eventId: 'evt-start-1', id: '2', runId: 'run-2', name: 'myWorkflow'
157
+ eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
130
158
  } );
131
159
  } );
132
160
  } );
133
161
 
134
162
  describe( 'onWorkflowEnd', () => {
135
- it( 'skips catalog workflow and forwards eventId for real workflows', async () => {
163
+ it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
136
164
  const handler = vi.fn().mockResolvedValue( undefined );
137
165
  onWorkflowEnd( handler );
138
166
 
139
167
  await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
140
- eventId: 'evt-ignored', id: '1', name: WORKFLOW_CATALOG, duration: 10
168
+ eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
141
169
  } ) );
142
170
  expect( handler ).not.toHaveBeenCalled();
143
171
 
144
172
  await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
145
- eventId: 'evt-end-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 5
173
+ eventId: 'evt-end-1', eventDate, workflowDetails, extra: 'passthrough'
146
174
  } ) );
147
175
  expect( handler ).toHaveBeenCalledWith( {
148
- eventId: 'evt-end-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 5
176
+ eventId: 'evt-end-1', eventDate, workflowDetails, extra: 'passthrough'
149
177
  } );
150
178
  } );
151
179
  } );
152
180
 
153
181
  describe( 'onWorkflowError', () => {
154
- it( 'skips catalog workflow and forwards eventId for real workflows', async () => {
182
+ it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
155
183
  const handler = vi.fn().mockResolvedValue( undefined );
156
184
  const err = new Error( 'wf' );
157
185
  onWorkflowError( handler );
158
186
 
159
187
  await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
160
- eventId: 'evt-ignored', id: '1', name: WORKFLOW_CATALOG, duration: 1, error: err
188
+ eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails, error: err
161
189
  } ) );
162
190
  expect( handler ).not.toHaveBeenCalled();
163
191
 
164
192
  await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
165
- eventId: 'evt-err-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 2, error: err
193
+ eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
166
194
  } ) );
167
195
  expect( handler ).toHaveBeenCalledWith( {
168
- eventId: 'evt-err-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 2, error: err
196
+ eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
169
197
  } );
170
198
  } );
171
199
  } );
@@ -1,8 +1,14 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
- import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy, continueAsNew } from '@temporalio/workflow';
2
+ import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy } from '@temporalio/workflow';
3
3
  import { defineSignal, setHandler } from '@temporalio/workflow';
4
4
  import { validateWorkflow } from './validations/static.js';
5
5
  import { validateWithSchema } from './validations/runtime.js';
6
+ import { deepMerge, setMetadata, toUrlSafeBase64 } from '#utils';
7
+ import { FatalError, ValidationError } from '#errors';
8
+ import { WorkflowContext } from '#internal_utils/workflow_context';
9
+ import { aggregateAttributes, mergeAggregations } from '#internal_utils/aggregations';
10
+ import { extractErrorDetail } from '#internal_utils/errors';
11
+ import { TraceInfo } from '#internal_utils/trace_info';
6
12
  import {
7
13
  ACTIVITY_GET_TRACE_DESTINATIONS,
8
14
  ACTIVITY_WRAPPER_VERSION_FIELD,
@@ -11,11 +17,6 @@ import {
11
17
  Signal,
12
18
  WORKFLOW_WRAPPER_VERSION_FIELD
13
19
  } from '#consts';
14
- import { deepMerge, setMetadata, toUrlSafeBase64 } from '#utils';
15
- import { FatalError, ValidationError } from '#errors';
16
- import { Context } from './workflow_context.js';
17
- import { aggregateAttributes, mergeAggregations } from '#internal_utils/aggregations';
18
- import { extractErrorDetail } from '#internal_utils/errors';
19
20
 
20
21
  const defaultOptions = {
21
22
  activityOptions: {
@@ -54,37 +55,21 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
54
55
  // this returns a plain function, for example, in unit tests
55
56
  if ( !inWorkflowContext() ) {
56
57
  validateWithSchema( inputSchema, input, `Workflow ${name} input` );
57
- const context = Context.build( {
58
- workflowId: 'test-workflow',
59
- runId: 'test-run',
60
- continueAsNew: async () => {},
61
- isContinueAsNewSuggested: () => false
62
- } );
63
- const output = await fn( input, deepMerge( context, extra.context ) );
58
+ const output = await fn( input, deepMerge( WorkflowContext.build(), extra.context ) );
64
59
  validateWithSchema( outputSchema, output, `Workflow ${name} output` );
65
60
  return output;
66
61
  }
67
62
 
68
- const { workflowId, runId, memo, startTime } = workflowInfo();
69
- const context = Context.build( { workflowId, runId, continueAsNew, isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested } );
70
-
71
- // Root workflows will not have the execution context yet, since it is set here.
72
- const isRoot = !memo.executionContext;
73
-
74
- /* Creates the execution context object or preserve if it already exists:
75
- It will always contain the information about the root workflow
76
- It will be used to as context for tracing (connecting events) */
77
- const executionContext = memo.executionContext ?? {
78
- workflowId,
79
- runId,
80
- workflowName: name,
81
- disableTrace,
82
- startTime: startTime.getTime()
83
- };
63
+ const { workflowId, memo, root } = workflowInfo();
64
+ const context = WorkflowContext.build();
65
+
66
+ const isRoot = !root;
84
67
 
68
+ // Creates the immutable memo that will be used by all nested workflows/activities
69
+ // Preserves all info that already exists in the memo object, so new child workflows inherit this
85
70
  Object.assign( memo, {
86
- executionContext,
87
- activityOptions: memo.activityOptions ?? activityOptions // Also preserve the original activity options
71
+ traceInfo: memo.traceInfo ?? TraceInfo.build( { disableTrace } ),
72
+ activityOptions: memo.activityOptions ?? activityOptions
88
73
  } );
89
74
 
90
75
  /**
@@ -94,7 +79,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
94
79
  * @TODO [OUT-468]
95
80
  */
96
81
  const getTraceDestinations = async () => {
97
- const result = await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( executionContext );
82
+ const result = await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo );
98
83
  return isActivityResultWrapped( result ) ? result.output : result;
99
84
  };
100
85
 
@@ -166,12 +151,11 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
166
151
  startWorkflow: async ( childName, input, extra = {} ) => {
167
152
  try {
168
153
  const result = await executeChild( childName, {
169
- args: input ? [ input ] : [],
154
+ args: undefined === input ? [] : [ input ],
170
155
  workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
171
156
  parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
172
157
  memo: {
173
- executionContext,
174
- parentId: workflowId,
158
+ ...memo,
175
159
  ...( extra?.options?.activityOptions && { activityOptions: deepMerge( activityOptions, extra.options.activityOptions ) } )
176
160
  }
177
161
  } );
@@ -5,6 +5,8 @@ import { z } from 'zod';
5
5
  const inWorkflowContextMock = vi.hoisted( () => vi.fn( () => true ) );
6
6
  const defineSignalMock = vi.hoisted( () => vi.fn( name => name ) );
7
7
  const setHandlerMock = vi.hoisted( () => vi.fn() );
8
+ const workflowContextBuildMock = vi.hoisted( () => vi.fn() );
9
+ const traceInfoBuildMock = vi.hoisted( () => vi.fn() );
8
10
  const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
9
11
  const executeChildMock = vi.fn().mockResolvedValue( undefined );
10
12
  const continueAsNewMock = vi.fn().mockResolvedValue( undefined );
@@ -31,6 +33,7 @@ const proxyActivitiesMock = vi.fn( () => {
31
33
  const workflowInfoReturn = {
32
34
  workflowId: 'wf-test-123',
33
35
  workflowType: 'test_wf',
36
+ runId: 'run-test-123',
34
37
  memo: {},
35
38
  startTime: new Date( '2025-01-01T00:00:00Z' ),
36
39
  continueAsNewSuggested: false
@@ -56,6 +59,14 @@ vi.mock( '@temporalio/workflow', () => ( {
56
59
  setHandler: ( ...args ) => setHandlerMock( ...args )
57
60
  } ) );
58
61
 
62
+ vi.mock( '#internal_utils/workflow_context', () => ( {
63
+ WorkflowContext: { build: workflowContextBuildMock }
64
+ } ) );
65
+
66
+ vi.mock( '#internal_utils/trace_info', () => ( {
67
+ TraceInfo: { build: traceInfoBuildMock }
68
+ } ) );
69
+
59
70
  vi.mock( '#consts', async importOriginal => {
60
71
  const actual = await importOriginal();
61
72
  return {
@@ -70,8 +81,20 @@ describe( 'workflow()', () => {
70
81
  vi.clearAllMocks();
71
82
  inWorkflowContextMock.mockReturnValue( true );
72
83
  defineSignalMock.mockImplementation( name => name );
73
- workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
74
84
  workflowInfoReturn.memo = {};
85
+ delete workflowInfoReturn.root;
86
+ workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
87
+ workflowContextBuildMock.mockReturnValue( {
88
+ control: {},
89
+ info: { workflowId: 'test-workflow', runId: 'test-run' }
90
+ } );
91
+ traceInfoBuildMock.mockImplementation( ( { disableTrace } ) => ( {
92
+ workflowId: 'trace-workflow-id',
93
+ workflowType: 'trace-workflow-type',
94
+ runId: 'trace-run-id',
95
+ startTime: 12345,
96
+ disableTrace
97
+ } ) );
75
98
  proxyActivitiesMock.mockImplementation( () => {
76
99
  stepSpyRef.current = vi.fn().mockResolvedValue( {} );
77
100
  return createStepsProxy( stepSpyRef.current );
@@ -230,7 +253,7 @@ describe( 'workflow()', () => {
230
253
  } );
231
254
 
232
255
  describe( 'root workflow (in workflow context)', () => {
233
- it( 'unwraps wrapped trace destinations and assigns executionContext to memo', async () => {
256
+ it( 'unwraps wrapped trace destinations and assigns traceInfo to memo', async () => {
234
257
  traceDestinationsStepMock.mockResolvedValueOnce( {
235
258
  output: { local: '/tmp/wrapped-trace' },
236
259
  aggregations: null,
@@ -255,12 +278,15 @@ describe( 'workflow()', () => {
255
278
  aggregations: null
256
279
  } );
257
280
  const memo = workflowInfoMock().memo;
258
- expect( memo.executionContext ).toEqual( {
259
- workflowId: 'wf-test-123',
260
- workflowName: 'wrapped_trace_wf',
261
- disableTrace: false,
262
- startTime: new Date( '2025-01-01T00:00:00Z' ).getTime()
263
- } );
281
+ expect( memo.traceInfo ).toEqual( {
282
+ workflowId: 'trace-workflow-id',
283
+ workflowType: 'trace-workflow-type',
284
+ runId: 'trace-run-id',
285
+ startTime: 12345,
286
+ disableTrace: false
287
+ } );
288
+ expect( traceInfoBuildMock ).toHaveBeenCalledWith( { disableTrace: false } );
289
+ expect( traceDestinationsStepMock ).toHaveBeenCalledWith( memo.traceInfo );
264
290
  } );
265
291
 
266
292
  it( 'collects batched aggregation signals from failed activities', async () => {
@@ -290,7 +316,7 @@ describe( 'workflow()', () => {
290
316
  expect( result.aggregations.httpRequests ).toEqual( { total: 1 } );
291
317
  } );
292
318
 
293
- it( 'sets executionContext.disableTrace when options.disableTrace is true', async () => {
319
+ it( 'sets traceInfo.disableTrace when options.disableTrace is true', async () => {
294
320
  const { workflow } = await import( './workflow.js' );
295
321
 
296
322
  const wf = workflow( {
@@ -303,15 +329,23 @@ describe( 'workflow()', () => {
303
329
  } );
304
330
 
305
331
  await wf( {} );
306
- expect( workflowInfoMock().memo.executionContext.disableTrace ).toBe( true );
332
+ expect( workflowInfoMock().memo.traceInfo ).toEqual( expect.objectContaining( {
333
+ workflowId: 'trace-workflow-id',
334
+ workflowType: 'trace-workflow-type',
335
+ runId: 'trace-run-id',
336
+ disableTrace: true
337
+ } ) );
338
+ expect( traceInfoBuildMock ).toHaveBeenCalledWith( { disableTrace: true } );
307
339
  } );
308
340
  } );
309
341
 
310
- describe( 'child workflow (memo.executionContext already set)', () => {
342
+ describe( 'child workflow (memo.traceInfo already set)', () => {
311
343
  it( 'does not call getTraceDestinations and returns an internal output envelope', async () => {
344
+ const traceInfo = { workflowId: 'parent-1', workflowType: 'parent_wf', runId: 'parent-run' };
312
345
  workflowInfoMock.mockReturnValue( {
313
346
  ...workflowInfoReturn,
314
- memo: { executionContext: { workflowId: 'parent-1', workflowName: 'parent_wf' } }
347
+ root: { workflowId: 'root-wf', runId: 'root-run' },
348
+ memo: { traceInfo }
315
349
  } );
316
350
  const { workflow } = await import( './workflow.js' );
317
351
 
@@ -325,6 +359,7 @@ describe( 'workflow()', () => {
325
359
 
326
360
  const result = await wf( {} );
327
361
  expect( traceDestinationsStepMock ).not.toHaveBeenCalled();
362
+ expect( workflowInfoMock().memo.traceInfo ).toBe( traceInfo );
328
363
  expect( result ).toEqual( {
329
364
  [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
330
365
  output: { x: 'child' },
@@ -445,10 +480,22 @@ describe( 'workflow()', () => {
445
480
  workflowId: expect.stringMatching( /^wf-test-123-/ ),
446
481
  parentClosePolicy: ParentClosePolicy.TERMINATE,
447
482
  memo: expect.objectContaining( {
448
- executionContext: expect.any( Object ),
449
- parentId: 'wf-test-123'
483
+ traceInfo: {
484
+ workflowId: 'trace-workflow-id',
485
+ workflowType: 'trace-workflow-type',
486
+ runId: 'trace-run-id',
487
+ startTime: 12345,
488
+ disableTrace: false
489
+ },
490
+ activityOptions: expect.objectContaining( {
491
+ startToCloseTimeout: '20m',
492
+ heartbeatTimeout: '5m'
493
+ } )
450
494
  } )
451
495
  } );
496
+ const [ , childOptions ] = executeChildMock.mock.calls[0];
497
+ expect( childOptions.memo ).not.toHaveProperty( 'executionContext' );
498
+ expect( childOptions.memo ).not.toHaveProperty( 'parentId' );
452
499
  } );
453
500
 
454
501
  it( 'uses ABANDON when extra.detached is true', async () => {
@@ -469,11 +516,12 @@ describe( 'workflow()', () => {
469
516
 
470
517
  await wf( {} );
471
518
  expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
519
+ args: [ null ],
472
520
  parentClosePolicy: ParentClosePolicy.ABANDON
473
521
  } ) );
474
522
  } );
475
523
 
476
- it( 'passes empty args when input is null/omitted', async () => {
524
+ it( 'passes empty args when input is omitted', async () => {
477
525
  const { workflow } = await import( './workflow.js' );
478
526
  executeChildMock.mockResolvedValueOnce( { output: {}, aggregations: null } );
479
527
 
@@ -494,6 +542,47 @@ describe( 'workflow()', () => {
494
542
  } ) );
495
543
  } );
496
544
 
545
+ it( 'merges per-child activity options into the propagated memo', async () => {
546
+ const { workflow } = await import( './workflow.js' );
547
+ executeChildMock.mockResolvedValueOnce( { output: {}, aggregations: null } );
548
+
549
+ const wf = workflow( {
550
+ name: 'child_options_wf',
551
+ description: 'Child options',
552
+ inputSchema: z.object( {} ),
553
+ outputSchema: z.object( {} ),
554
+ async fn() {
555
+ await this.startWorkflow( 'child_wf', { id: 1 }, {
556
+ options: {
557
+ activityOptions: {
558
+ startToCloseTimeout: '2m',
559
+ retry: { maximumAttempts: 7 }
560
+ }
561
+ }
562
+ } );
563
+ return {};
564
+ }
565
+ } );
566
+
567
+ await wf( {} );
568
+ expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
569
+ memo: expect.objectContaining( {
570
+ traceInfo: expect.objectContaining( {
571
+ workflowId: 'trace-workflow-id',
572
+ runId: 'trace-run-id'
573
+ } ),
574
+ activityOptions: expect.objectContaining( {
575
+ startToCloseTimeout: '2m',
576
+ heartbeatTimeout: '5m',
577
+ retry: expect.objectContaining( {
578
+ initialInterval: '10s',
579
+ maximumAttempts: 7
580
+ } )
581
+ } )
582
+ } )
583
+ } ) );
584
+ } );
585
+
497
586
  it( 'returns child output and merges child workflow aggregations into the root aggregations', async () => {
498
587
  const { workflow } = await import( './workflow.js' );
499
588
  executeChildMock.mockResolvedValueOnce( {
@@ -56,11 +56,11 @@ export const sendHttpRequest = async ( { url, method, payload = undefined, heade
56
56
  setMetadata( sendHttpRequest, { type: ComponentType.INTERNAL_STEP } );
57
57
 
58
58
  /**
59
- * Invokes a trace method that resolves all trace output paths based on the executionContext
59
+ * Invokes a trace method that resolves all trace output paths based on the traceInfo
60
60
  *
61
- * @param {object} executionContext
61
+ * @param {object} traceInfo
62
62
  * @returns {object} Information about enabled destinations
63
63
  */
64
- export const getTraceDestinations = executionContext => getDestinations( executionContext );
64
+ export const getTraceDestinations = traceInfo => getDestinations( traceInfo );
65
65
 
66
66
  setMetadata( getTraceDestinations, { type: ComponentType.INTERNAL_STEP } );
@@ -2,7 +2,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { MockAgent, setGlobalDispatcher } from 'undici';
3
3
  import { FatalError } from '#errors';
4
4
  import { serializeBodyAndInferContentType, serializeFetchResponse } from '#utils';
5
- import { sendHttpRequest } from './index.js';
5
+ import { getTraceDestinations, sendHttpRequest } from './index.js';
6
+
7
+ const getDestinationsMock = vi.hoisted( () => vi.fn() );
8
+
9
+ vi.mock( '#tracing', () => ( {
10
+ getDestinations: getDestinationsMock
11
+ } ) );
6
12
 
7
13
  vi.mock( '#logger', () => {
8
14
  const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
@@ -100,3 +106,27 @@ describe( 'internal_activities/sendHttpRequest', () => {
100
106
  expect( serializeBodyAndInferContentType ).not.toHaveBeenCalled();
101
107
  } );
102
108
  } );
109
+
110
+ describe( 'internal_activities/getTraceDestinations', () => {
111
+ beforeEach( () => {
112
+ vi.clearAllMocks();
113
+ } );
114
+
115
+ it( 'returns trace destinations for the given traceInfo', () => {
116
+ const traceInfo = {
117
+ workflowId: 'workflow-id',
118
+ runId: 'run-id',
119
+ workflowType: 'workflow',
120
+ startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
121
+ disableTrace: false
122
+ };
123
+ const destinations = {
124
+ local: '/tmp/project/logs/runs/workflow/trace.json',
125
+ remote: null
126
+ };
127
+ getDestinationsMock.mockReturnValueOnce( destinations );
128
+
129
+ expect( getTraceDestinations( traceInfo ) ).toBe( destinations );
130
+ expect( getDestinationsMock ).toHaveBeenCalledWith( traceInfo );
131
+ } );
132
+ } );
@@ -0,0 +1,12 @@
1
+ export const createWorkflowDetails = info => ( {
2
+ attempt: info.attempt,
3
+ continuedFromExecutionRunId: info.continuedFromExecutionRunId,
4
+ firstExecutionRunId: info.firstExecutionRunId,
5
+ parent: info.parent,
6
+ root: info.root,
7
+ runId: info.runId,
8
+ runStartTime: info.runStartTime.getTime(),
9
+ startTime: info.startTime.getTime(),
10
+ workflowId: info.workflowId,
11
+ workflowType: info.workflowType
12
+ } );
@@ -0,0 +1,83 @@
1
+ import type { WorkflowInfo } from '@temporalio/workflow';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createWorkflowDetails } from './temporal_context.js';
4
+
5
+ /*
6
+ This spec is TypeScript on purpose.
7
+ createWorkflowDetails() accepts a small projection of Temporal's WorkflowInfo,
8
+ so the fixture below uses Pick<WorkflowInfo, ...> to catch Temporal type drift
9
+ without exposing or maintaining the full native object in hook payloads.
10
+ */
11
+ type WorkflowDetailsSource = Pick<
12
+ WorkflowInfo,
13
+ 'attempt' |
14
+ 'continuedFromExecutionRunId' |
15
+ 'firstExecutionRunId' |
16
+ 'parent' |
17
+ 'root' |
18
+ 'runId' |
19
+ 'runStartTime' |
20
+ 'startTime' |
21
+ 'workflowId' |
22
+ 'workflowType'
23
+ >;
24
+
25
+ describe( 'createWorkflowDetails', () => {
26
+ it( 'creates hook-safe workflow details from Temporal workflow info', () => {
27
+ const parent = { workflowId: 'parent-wf', runId: 'parent-run', namespace: 'default' };
28
+ const root = { workflowId: 'root-wf', runId: 'root-run' };
29
+ const workflowInfo = {
30
+ attempt: 2,
31
+ continuedFromExecutionRunId: 'previous-run',
32
+ firstExecutionRunId: 'first-run',
33
+ parent,
34
+ root,
35
+ runId: 'current-run',
36
+ runStartTime: new Date( '2026-06-02T09:30:00.000Z' ),
37
+ startTime: new Date( '2026-06-02T09:00:00.000Z' ),
38
+ workflowId: 'workflow-id',
39
+ workflowType: 'prompt'
40
+ } satisfies WorkflowDetailsSource;
41
+
42
+ expect( createWorkflowDetails( workflowInfo ) ).toEqual( {
43
+ attempt: 2,
44
+ continuedFromExecutionRunId: 'previous-run',
45
+ firstExecutionRunId: 'first-run',
46
+ parent,
47
+ root,
48
+ runId: 'current-run',
49
+ runStartTime: Date.parse( '2026-06-02T09:30:00.000Z' ),
50
+ startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
51
+ workflowId: 'workflow-id',
52
+ workflowType: 'prompt'
53
+ } );
54
+ } );
55
+
56
+ it( 'preserves absent optional workflow relationships as undefined', () => {
57
+ const workflowInfo = {
58
+ attempt: 1,
59
+ continuedFromExecutionRunId: undefined,
60
+ firstExecutionRunId: 'first-run',
61
+ parent: undefined,
62
+ root: undefined,
63
+ runId: 'current-run',
64
+ runStartTime: new Date( '2026-06-02T09:30:00.000Z' ),
65
+ startTime: new Date( '2026-06-02T09:00:00.000Z' ),
66
+ workflowId: 'workflow-id',
67
+ workflowType: 'prompt'
68
+ } satisfies WorkflowDetailsSource;
69
+
70
+ expect( createWorkflowDetails( workflowInfo ) ).toEqual( {
71
+ attempt: 1,
72
+ continuedFromExecutionRunId: undefined,
73
+ firstExecutionRunId: 'first-run',
74
+ parent: undefined,
75
+ root: undefined,
76
+ runId: 'current-run',
77
+ runStartTime: Date.parse( '2026-06-02T09:30:00.000Z' ),
78
+ startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
79
+ workflowId: 'workflow-id',
80
+ workflowType: 'prompt'
81
+ } );
82
+ } );
83
+ } );
@@ -0,0 +1,21 @@
1
+ import { inWorkflowContext, workflowInfo } from '@temporalio/workflow';
2
+
3
+ export class TraceInfo {
4
+
5
+ /**
6
+ * Builds the trace information propagated through workflow memo and activity headers.
7
+ * @param {object} options - Arguments to build trace information
8
+ * @param {boolean} options.disableTrace - Whether trace event emission should be disabled
9
+ * @returns {object} trace information
10
+ */
11
+ static build( { disableTrace } ) {
12
+ const info = inWorkflowContext() ? workflowInfo() : {};
13
+ return {
14
+ workflowId: info.workflowId,
15
+ workflowType: info.workflowType,
16
+ runId: info.runId,
17
+ startTime: info.startTime?.getTime(),
18
+ disableTrace
19
+ };
20
+ };
21
+ }