@outputai/core 0.10.1-next.fc0a41f.0 → 0.11.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 (63) hide show
  1. package/package.json +12 -7
  2. package/src/consts.js +5 -2
  3. package/src/errors.d.ts +12 -2
  4. package/src/errors.js +17 -3
  5. package/src/errors.spec.js +19 -0
  6. package/src/helpers/activity_options.js +21 -0
  7. package/src/helpers/activity_options.spec.js +55 -0
  8. package/src/helpers/error_serializer.js +236 -0
  9. package/src/helpers/error_serializer.spec.js +413 -0
  10. package/src/helpers/errors.js +55 -13
  11. package/src/helpers/errors.spec.js +166 -66
  12. package/src/helpers/fetch.js +1 -38
  13. package/src/helpers/fetch.spec.js +1 -39
  14. package/src/helpers/function.js +13 -0
  15. package/src/helpers/function.spec.js +23 -1
  16. package/src/helpers/redact.js +51 -0
  17. package/src/helpers/redact.spec.js +66 -0
  18. package/src/helpers/string.js +10 -0
  19. package/src/helpers/string.spec.js +36 -1
  20. package/src/hooks/index.d.ts +35 -44
  21. package/src/hooks/index.js +2 -1
  22. package/src/hooks/index.spec.js +15 -7
  23. package/src/interface/evaluator.d.ts +10 -9
  24. package/src/interface/evaluator.js +1 -6
  25. package/src/interface/evaluator.spec.js +10 -10
  26. package/src/interface/step.d.ts +16 -15
  27. package/src/interface/step.js +1 -6
  28. package/src/interface/step.spec.js +10 -10
  29. package/src/interface/validations/index.js +33 -53
  30. package/src/interface/validations/index.spec.js +25 -17
  31. package/src/interface/webhook.js +1 -2
  32. package/src/interface/webhook.spec.js +25 -19
  33. package/src/interface/workflow.d.ts +25 -18
  34. package/src/interface/workflow.js +24 -52
  35. package/src/interface/workflow.spec.js +109 -108
  36. package/src/interface/workflow_activity_options.js +2 -5
  37. package/src/temporal/client.js +47 -0
  38. package/src/temporal/client.spec.js +124 -0
  39. package/src/temporal/index.d.ts +32 -0
  40. package/src/temporal/index.js +1 -0
  41. package/src/tracing/trace_engine.js +9 -11
  42. package/src/tracing/trace_engine.spec.js +67 -28
  43. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  44. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  45. package/src/worker/index.js +10 -1
  46. package/src/worker/index.spec.js +23 -8
  47. package/src/worker/interceptors/activity.js +32 -19
  48. package/src/worker/interceptors/activity.spec.js +71 -166
  49. package/src/worker/interceptors/workflow.js +53 -11
  50. package/src/worker/interceptors/workflow.spec.js +176 -49
  51. package/src/worker/loader/workflows.js +16 -3
  52. package/src/worker/loader/workflows.spec.js +37 -5
  53. package/src/worker/log_hooks.js +13 -10
  54. package/src/worker/log_hooks.spec.js +12 -4
  55. package/src/worker/sinks.js +3 -1
  56. package/src/worker/sinks.spec.js +21 -4
  57. package/src/worker/telemetry.js +2 -1
  58. package/src/worker/temporal_logger.js +33 -0
  59. package/src/worker/temporal_logger.spec.js +90 -0
  60. package/src/helpers/aggregations.js +0 -44
  61. package/src/helpers/aggregations.spec.js +0 -92
  62. package/src/tracing/tools/utils.js +0 -42
  63. package/src/tracing/tools/utils.spec.js +0 -83
@@ -0,0 +1,32 @@
1
+ import type { Client, WorkflowHandle } from '@temporalio/client';
2
+
3
+ /**
4
+ * Worker-side access to the Temporal layer.
5
+ *
6
+ * `createTemporalClient()` is available after the worker establishes its Temporal
7
+ * connection. `getCurrentWorkflowHandle()` is only available from Temporal Activities.
8
+ *
9
+ * > [!WARNING]
10
+ * > These are not Temporal sandbox safe, do not import in workflows directly.
11
+ * > These require the worker runtime, so in unit tests, mock the functions.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ /**
17
+ * Creates a Temporal [Client](https://typescript.temporal.io/api/classes/client.Client)
18
+ * instance with the same connection and namespace as the worker.
19
+ *
20
+ * @remarks
21
+ * - Only available after the worker establishes its Temporal connection.
22
+ */
23
+ export declare const createTemporalClient: () => Client;
24
+
25
+ /**
26
+ * Return the [WorkflowHandle](https://typescript.temporal.io/api/interfaces/client.WorkflowHandle)
27
+ * for the current workflow execution.
28
+ *
29
+ * @remarks
30
+ * - Only available from a Temporal Activity running in the worker.
31
+ */
32
+ export declare const getCurrentWorkflowHandle: () => WorkflowHandle;
@@ -0,0 +1 @@
1
+ export { createTemporalClient, getCurrentWorkflowHandle } from './client.js';
@@ -1,12 +1,13 @@
1
1
  import { Storage } from '#async_storage';
2
2
  import { EventEmitter } from 'node:events';
3
- import { serializeError } from './tools/utils.js';
3
+ import { serializeError } from '#helpers/error_serializer';
4
4
  import { isStringboolTrue } from '#helpers/string';
5
5
  import * as localProcessor from './processors/local/index.js';
6
6
  import * as s3Processor from './processors/s3/index.js';
7
7
  import { createChildLogger } from '#logger';
8
8
  import { EventAction } from './trace_consts.js';
9
9
  import { BaseAttribute } from './trace_attribute.js';
10
+ import { TransparentFatalError } from '#errors';
10
11
 
11
12
  const log = createChildLogger( 'Tracing' );
12
13
 
@@ -51,7 +52,7 @@ export const init = async () => {
51
52
  try {
52
53
  await p.exec( ...args );
53
54
  } catch ( error ) {
54
- log.error( 'Processor execution error', { processor: p.name, error: error.message, stack: error.stack } );
55
+ log.error( 'Processor execution error', { processor: p.name, error: serializeError( error ) } );
55
56
  }
56
57
  } );
57
58
  }
@@ -87,15 +88,12 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, tra
87
88
  export function addEventActionWithContext( action, options ) {
88
89
  const storeContent = Storage.load();
89
90
  if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
90
- const { parentId, traceInfo, addAttribute } = storeContent;
91
- if ( action === EventAction.ADD_ATTR ) {
92
- const attribute = options.details;
93
- if ( !( attribute instanceof BaseAttribute ) ) {
94
- throw new Error( `Event ${EventAction.ADD_ATTR} argument is not a BaseAttribute instance` );
95
- } else {
96
- addAttribute( options.details );
97
- }
91
+ const { parentId, traceInfo } = storeContent;
92
+ if ( action === EventAction.ADD_ATTR && !( options.details instanceof BaseAttribute ) ) {
93
+ throw new Error( `Event ${EventAction.ADD_ATTR} argument is not a BaseAttribute instance` );
98
94
  }
99
- addEventAction( action, { ...options, parentId, traceInfo } );
95
+ /** Unwraps Transparent error set in details so it behaves like in the interceptors */
96
+ const details = options.details instanceof TransparentFatalError ? options.details.cause : options.details;
97
+ addEventAction( action, { ...options, details, parentId, traceInfo } );
100
98
  }
101
99
  };
@@ -5,10 +5,15 @@ vi.mock( '#async_storage', () => ( {
5
5
  Storage: { load: storageLoadMock }
6
6
  } ) );
7
7
 
8
- const logWarnMock = vi.fn();
8
+ const serializeErrorMock = vi.fn( error => ( {
9
+ name: error.name,
10
+ message: error.message
11
+ } ) );
12
+ vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
13
+
9
14
  const logErrorMock = vi.fn();
10
15
  vi.mock( '#logger', () => ( {
11
- createChildLogger: () => ( { warn: logWarnMock, error: logErrorMock } )
16
+ createChildLogger: () => ( { error: logErrorMock } )
12
17
  } ) );
13
18
 
14
19
  const localInitMock = vi.fn( async () => {} );
@@ -71,6 +76,24 @@ describe( 'tracing/trace_engine', () => {
71
76
  expect( payload.traceInfo ).toBe( traceInfo );
72
77
  } );
73
78
 
79
+ it( 'init() logs processor failures with serialized stacks', async () => {
80
+ process.env.OUTPUT_TRACE_LOCAL_ON = '1';
81
+ const processorError = new Error( 'processor failed' );
82
+ localExecMock.mockRejectedValueOnce( processorError );
83
+ const { init, addEventAction } = await loadTraceEngine();
84
+ await init();
85
+
86
+ addEventAction( 'start', {
87
+ kind: 'step', name: 'N', id: '1', parentId: 'p', details: null, traceInfo
88
+ } );
89
+
90
+ await vi.waitFor( () => expect( logErrorMock ).toHaveBeenCalledWith( 'Processor execution error', {
91
+ processor: 'LOCAL',
92
+ error: { name: 'Error', message: 'processor failed' }
93
+ } ) );
94
+ expect( serializeErrorMock ).toHaveBeenCalledWith( processorError );
95
+ } );
96
+
74
97
  it( 'addEventAction() emits an entry consumed by processors', async () => {
75
98
  process.env.OUTPUT_TRACE_LOCAL_ON = 'on';
76
99
  const { init, addEventAction } = await loadTraceEngine();
@@ -87,34 +110,34 @@ describe( 'tracing/trace_engine', () => {
87
110
  expect( payload.entry.details ).toBe( 'done' );
88
111
  } );
89
112
 
90
- it( 'addEventAction() does not emit when traceInfo is absent', async () => {
91
- process.env.OUTPUT_TRACE_LOCAL_ON = '1';
113
+ it( 'addEventAction() serializes error details before emitting', async () => {
114
+ process.env.OUTPUT_TRACE_LOCAL_ON = 'on';
92
115
  const { init, addEventAction } = await loadTraceEngine();
93
116
  await init();
117
+ const error = new TypeError( 'step failed' );
94
118
 
95
- addEventAction( 'start', {
96
- kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
97
- traceInfo: undefined
119
+ addEventAction( 'error', {
120
+ kind: 'step', name: 'S', id: '3', parentId: 'p3', details: error,
121
+ traceInfo
122
+ } );
123
+
124
+ expect( serializeErrorMock ).toHaveBeenCalledWith( error );
125
+ expect( localExecMock.mock.calls[0][0].entry.details ).toEqual( {
126
+ name: 'TypeError',
127
+ message: 'step failed'
98
128
  } );
99
- expect( localExecMock ).not.toHaveBeenCalled();
100
129
  } );
101
130
 
102
- it( 'addEventAction() emits when kind is INTERNAL_STEP', async () => {
131
+ it( 'addEventAction() does not emit when traceInfo is absent', async () => {
103
132
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
104
133
  const { init, addEventAction } = await loadTraceEngine();
105
134
  await init();
106
135
 
107
136
  addEventAction( 'start', {
108
- kind: 'internal_step', name: 'Internal', id: '1', parentId: 'p', details: {},
109
- traceInfo
110
- } );
111
- expect( localExecMock ).toHaveBeenCalledTimes( 1 );
112
- expect( localExecMock.mock.calls[0][0].entry ).toMatchObject( {
113
- kind: 'internal_step',
114
- name: 'Internal',
115
- id: '1',
116
- parentId: 'p'
137
+ kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
138
+ traceInfo: undefined
117
139
  } );
140
+ expect( localExecMock ).not.toHaveBeenCalled();
118
141
  } );
119
142
 
120
143
  it( 'addEventActionWithContext() uses storage when available', async () => {
@@ -135,13 +158,34 @@ describe( 'tracing/trace_engine', () => {
135
158
  expect( payload.entry.action ).toBe( 'tick' );
136
159
  } );
137
160
 
138
- it( 'addEventActionWithContext() records ADD_ATTR attributes through storage context', async () => {
161
+ it( 'addEventActionWithContext() unwraps transparent errors without mutating options', async () => {
139
162
  process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
140
- const addAttributeMock = vi.fn();
141
163
  storageLoadMock.mockReturnValue( {
142
164
  parentId: 'ctx-p',
143
- traceInfo,
144
- addAttribute: addAttributeMock
165
+ traceInfo
166
+ } );
167
+ const { init, addEventActionWithContext } = await loadTraceEngine();
168
+ const { TransparentFatalError } = await import( '#errors' );
169
+ await init();
170
+
171
+ const cause = new TypeError( 'provider rejected request' );
172
+ const error = new TransparentFatalError( cause );
173
+ const options = { kind: 'llm', name: 'generate', id: 'llm-1', details: error };
174
+ addEventActionWithContext( 'error', options );
175
+
176
+ expect( options.details ).toBe( error );
177
+ expect( serializeErrorMock ).toHaveBeenCalledWith( cause );
178
+ expect( localExecMock.mock.calls[0][0].entry.details ).toEqual( {
179
+ name: 'TypeError',
180
+ message: 'provider rejected request'
181
+ } );
182
+ } );
183
+
184
+ it( 'addEventActionWithContext() emits validated ADD_ATTR trace entries', async () => {
185
+ process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
186
+ storageLoadMock.mockReturnValue( {
187
+ parentId: 'ctx-p',
188
+ traceInfo
145
189
  } );
146
190
  const { init, addEventActionWithContext } = await loadTraceEngine();
147
191
  const { EventAction } = await import( './trace_consts.js' );
@@ -151,8 +195,6 @@ describe( 'tracing/trace_engine', () => {
151
195
  const attribute = new Attribute.HTTPRequestCount( 'https://example.test', 'req-1' );
152
196
  addEventActionWithContext( EventAction.ADD_ATTR, { kind: 'http', name: 'request', id: 'req-1', details: attribute } );
153
197
 
154
- expect( addAttributeMock ).toHaveBeenCalledTimes( 1 );
155
- expect( addAttributeMock ).toHaveBeenCalledWith( attribute );
156
198
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
157
199
  expect( localExecMock.mock.calls[0][0] ).toEqual( {
158
200
  traceInfo,
@@ -170,11 +212,9 @@ describe( 'tracing/trace_engine', () => {
170
212
 
171
213
  it( 'addEventActionWithContext() throws on invalid ADD_ATTR signal payloads', async () => {
172
214
  process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
173
- const addAttributeMock = vi.fn();
174
215
  storageLoadMock.mockReturnValue( {
175
216
  parentId: 'ctx-p',
176
- traceInfo,
177
- addAttribute: addAttributeMock
217
+ traceInfo
178
218
  } );
179
219
  const { init, addEventActionWithContext } = await loadTraceEngine();
180
220
  const { EventAction } = await import( './trace_consts.js' );
@@ -186,7 +226,6 @@ describe( 'tracing/trace_engine', () => {
186
226
  { kind: 'http', name: 'request', id: 'req-1', details: invalidAttribute }
187
227
  ) ).toThrow( /not a BaseAttribute instance/ );
188
228
 
189
- expect( addAttributeMock ).not.toHaveBeenCalled();
190
229
  expect( localExecMock ).not.toHaveBeenCalled();
191
230
  } );
192
231
 
@@ -38,7 +38,7 @@ export class CatalogJob {
38
38
  throw e;
39
39
  }
40
40
  if ( !( e instanceof WorkflowNotFoundError ) ) {
41
- log.warn( 'Error describing catalog workflow', { error: e } );
41
+ log.warn( 'Error describing catalog workflow', { error: e.message } );
42
42
  }
43
43
  return false;
44
44
  } );
@@ -52,7 +52,7 @@ export class CatalogJob {
52
52
  throw e;
53
53
  }
54
54
  if ( !( e instanceof WorkflowNotFoundError ) ) {
55
- log.warn( 'Error describing catalog workflow', { error: e } );
55
+ log.warn( 'Error describing catalog workflow', { error: e.message } );
56
56
  }
57
57
  return false;
58
58
  } );
@@ -66,7 +66,7 @@ export class CatalogJob {
66
66
  throw e;
67
67
  }
68
68
  if ( !( e instanceof WorkflowNotFoundError ) ) {
69
- log.warn( 'Error completing previous catalog workflow', { error: e } );
69
+ log.warn( 'Error completing previous catalog workflow', { error: e.message } );
70
70
  }
71
71
  } );
72
72
  };
@@ -121,7 +121,7 @@ describe( 'CatalogJob', () => {
121
121
 
122
122
  await job.run();
123
123
 
124
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Error completing previous catalog workflow', { error: completeError } );
124
+ expect( mockLog.warn ).toHaveBeenCalledWith( 'Error completing previous catalog workflow', { error: completeError.message } );
125
125
  expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', expect.any( Object ) );
126
126
  expect( job.error ).toBeNull();
127
127
  } );
@@ -18,7 +18,10 @@ import { BusEventType } from '#consts';
18
18
  import { setupTelemetry } from './telemetry.js';
19
19
  import { TemporalConnectionMonitor } from './connection_monitor.js';
20
20
  import { bindGlobalFunctions } from './global_functions.js';
21
+ import { setupClientConfig } from '#temporal/client';
21
22
  import { runOnce } from '#helpers/function';
23
+ import { serializeError } from '#helpers/error_serializer';
24
+ import { setupTemporalLogger } from './temporal_logger.js';
22
25
 
23
26
  import './log_hooks.js';
24
27
 
@@ -51,6 +54,9 @@ const state = {
51
54
  const callerDir = process.argv[2];
52
55
 
53
56
  const execute = async () => {
57
+ log.info( 'Setup Temporal Logger' );
58
+ setupTemporalLogger();
59
+
54
60
  log.info( 'Loading config...', { callerDir } );
55
61
  await loadHooks( callerDir );
56
62
 
@@ -81,6 +87,9 @@ const execute = async () => {
81
87
 
82
88
  state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
83
89
 
90
+ // add configs for the /temporal endpoint
91
+ setupClientConfig( { connection: state.connection, namespace } );
92
+
84
93
  log.info( 'Creating connection monitor...' );
85
94
  state.connectionMonitor = new TemporalConnectionMonitor( state.connection );
86
95
 
@@ -219,7 +228,7 @@ execute()
219
228
  log.info( 'Bye' );
220
229
  } )
221
230
  .catch( async error => {
222
- log.error( 'Fatal error', { error: error.message, stack: error.stack } );
231
+ log.error( 'Fatal error', { error: serializeError( error ) } );
223
232
 
224
233
  mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
225
234
 
@@ -14,7 +14,10 @@ const {
14
14
  mockWorker,
15
15
  promises,
16
16
  resetPromises,
17
+ serializeErrorMock,
18
+ setupClientConfigMock,
17
19
  setupInterruptionHandlerMock,
20
+ setupTemporalLoggerMock,
18
21
  setupTelemetryMock
19
22
  } = vi.hoisted( () => {
20
23
  const createDeferred = () => {
@@ -117,16 +120,20 @@ const {
117
120
  mockWorker,
118
121
  promises,
119
122
  resetPromises,
123
+ serializeErrorMock: vi.fn( error => ( {
124
+ name: error.name,
125
+ message: error.message,
126
+ stack: error.stack
127
+ } ) ),
128
+ setupClientConfigMock: vi.fn(),
120
129
  setupInterruptionHandlerMock: vi.fn(),
130
+ setupTemporalLoggerMock: vi.fn(),
121
131
  setupTelemetryMock: vi.fn()
122
132
  };
123
133
  } );
124
134
 
125
135
  vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
126
- vi.mock( '#consts', async importOriginal => {
127
- const actual = await importOriginal();
128
- return { ...actual };
129
- } );
136
+ vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
130
137
  const initTracing = vi.fn().mockResolvedValue( undefined );
131
138
  vi.mock( '#tracing', () => ( { init: initTracing } ) );
132
139
  vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
@@ -161,6 +168,8 @@ vi.mock( './catalog_workflow/catalog_job.js', () => ( {
161
168
  } )
162
169
  } ) );
163
170
  vi.mock( './log_hooks.js', () => ( {} ) );
171
+ vi.mock( './temporal_logger.js', () => ( { setupTemporalLogger: setupTemporalLoggerMock } ) );
172
+ vi.mock( '#temporal/client', () => ( { setupClientConfig: setupClientConfigMock } ) );
164
173
  vi.mock( '@temporalio/worker', () => ( {
165
174
  NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) },
166
175
  Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
@@ -217,6 +226,7 @@ describe( 'worker/index', () => {
217
226
  await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalled() );
218
227
 
219
228
  expect( loadHooksMock ).toHaveBeenCalledWith( '/test/caller/dir' );
229
+ expect( setupTemporalLoggerMock ).toHaveBeenCalledOnce();
220
230
  expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
221
231
  expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
222
232
  expect( initTracing ).toHaveBeenCalled();
@@ -228,6 +238,10 @@ describe( 'worker/index', () => {
228
238
  apiKey: undefined,
229
239
  proxy: undefined
230
240
  } );
241
+ expect( setupClientConfigMock ).toHaveBeenCalledWith( {
242
+ connection: mockConnection,
243
+ namespace: configValues.namespace
244
+ } );
231
245
  expect( bindGlobalFunctionsMock ).toHaveBeenCalledTimes( 1 );
232
246
  expect( bindGlobalFunctionsMock.mock.invocationCallOrder[0] ).toBeLessThan(
233
247
  NativeConnection.connect.mock.invocationCallOrder[0]
@@ -356,9 +370,10 @@ describe( 'worker/index', () => {
356
370
 
357
371
  await vi.waitFor( () => {
358
372
  expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
359
- error: 'Big Failure'
373
+ error: expect.objectContaining( { message: 'Big Failure' } )
360
374
  } ) );
361
375
  } );
376
+ expect( serializeErrorMock ).toHaveBeenCalledWith( error );
362
377
  expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
363
378
  await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
364
379
  expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
@@ -378,7 +393,7 @@ describe( 'worker/index', () => {
378
393
 
379
394
  await vi.waitFor( () => {
380
395
  expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
381
- error: 'connection lost'
396
+ error: expect.objectContaining( { message: 'connection lost' } )
382
397
  } ) );
383
398
  } );
384
399
  expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
@@ -398,7 +413,7 @@ describe( 'worker/index', () => {
398
413
 
399
414
  await vi.waitFor( () => {
400
415
  expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
401
- error: 'catalog failed'
416
+ error: expect.objectContaining( { message: 'catalog failed' } )
402
417
  } ) );
403
418
  } );
404
419
  expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
@@ -418,7 +433,7 @@ describe( 'worker/index', () => {
418
433
  expect( catalogJobInstance.interrupt ).not.toHaveBeenCalled();
419
434
  await vi.waitFor( () => {
420
435
  expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
421
- error: 'worker create failed'
436
+ error: expect.objectContaining( { message: 'worker create failed' } )
422
437
  } ) );
423
438
  } );
424
439
  } );
@@ -1,12 +1,14 @@
1
- import { Context, activityInfo as activityInfoFn } from '@temporalio/activity';
1
+ import { Context, activityInfo as activityInfoFn, ApplicationFailure } from '@temporalio/activity';
2
+ import { CompleteAsyncError, TemporalFailure } from '@temporalio/common';
2
3
  import { Storage } from '#async_storage';
3
4
  import * as Tracing from '#tracing';
4
5
  import { headersToObject } from './headers.js';
5
- import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
+ import { ActivitySpecialOutput, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
7
  import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
7
8
  import { mainEventBus } from '#bus';
8
- import { aggregateAttributes } from '#helpers/aggregations';
9
- import { buildApplicationFailureWithDetails } from '#helpers/errors';
9
+ import { inheritsFromAnyNamedType } from '#helpers/errors';
10
+ import { serializeError } from '#helpers/error_serializer';
11
+ import { FatalError, TransparentFatalError } from '#errors';
10
12
 
11
13
  /*
12
14
  This interceptor wraps every activity execution with cross-cutting concerns:
@@ -51,12 +53,9 @@ export class ActivityExecutionInterceptor {
51
53
  }
52
54
 
53
55
  const state = {
54
- heartbeat: null,
55
- attributes: []
56
+ heartbeat: null
56
57
  };
57
58
 
58
- const addAttribute = attribute => state.attributes.push( attribute );
59
-
60
59
  // Adds context accessible information
61
60
  const storageContext = {
62
61
  parentId: activityId,
@@ -64,8 +63,7 @@ export class ActivityExecutionInterceptor {
64
63
  activityInfo,
65
64
  workflowDetails,
66
65
  workflowFilename,
67
- traceInfo,
68
- addAttribute
66
+ traceInfo
69
67
  };
70
68
 
71
69
  mainEventBus.emit( BusEventType.ACTIVITY_START, { activityInfo, workflowDetails, outputActivityKind } );
@@ -77,19 +75,34 @@ export class ActivityExecutionInterceptor {
77
75
 
78
76
  const output = await Storage.runWithContext( async _ => next( input ), storageContext );
79
77
 
80
- const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
81
-
82
- mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, aggregations, workflowDetails, outputActivityKind } );
78
+ mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, workflowDetails, outputActivityKind } );
83
79
  Tracing.addEventEnd( { id: activityId, details: output, traceInfo } );
84
80
 
85
- return { [ACTIVITY_WRAPPER_VERSION_FIELD]: 1, output, aggregations };
81
+ return output;
86
82
 
87
83
  } catch ( error ) {
88
- const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
89
- mainEventBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, aggregations, workflowDetails, outputActivityKind, error } );
90
- Tracing.addEventError( { id: activityId, details: error, traceInfo } );
91
-
92
- throw aggregations ? buildApplicationFailureWithDetails( error, { aggregations } ) : error;
84
+ // Record async completion handoff as a trace end without emitting a bus event.
85
+ if ( error instanceof CompleteAsyncError ) {
86
+ Tracing.addEventEnd( { id: activityId, details: ActivitySpecialOutput.ASYNC_HANDOFF, traceInfo } );
87
+ throw error;
88
+ }
89
+
90
+ const unwrappedError = error instanceof TransparentFatalError ? error.cause : error;
91
+
92
+ Tracing.addEventError( { id: activityId, details: unwrappedError, traceInfo } );
93
+ mainEventBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error: unwrappedError } );
94
+
95
+ // Native Temporal errors are just re-thrown
96
+ if ( error instanceof TemporalFailure ) {
97
+ throw error;
98
+ }
99
+
100
+ const nonRetryable = error instanceof FatalError || inheritsFromAnyNamedType( error, activityInfo.retryPolicy?.nonRetryableErrorTypes ?? [] );
101
+ throw ApplicationFailure.fromError( unwrappedError, {
102
+ nonRetryable,
103
+ cause: unwrappedError,
104
+ details: [ { error: serializeError( unwrappedError, { dropKeys: [ 'stack' ] } ) } ]
105
+ } );
93
106
  } finally {
94
107
  clearInterval( state.heartbeat );
95
108
  }