@outputai/core 0.4.1-dev.622e67b.0 → 0.4.1-dev.6555a2c.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 (34) hide show
  1. package/package.json +5 -1
  2. package/src/activity_integration/events.d.ts +6 -1
  3. package/src/activity_integration/events.js +2 -2
  4. package/src/activity_integration/events.spec.js +87 -0
  5. package/src/activity_integration/tracing.d.ts +5 -10
  6. package/src/activity_integration/tracing.js +4 -9
  7. package/src/consts.js +4 -0
  8. package/src/hooks/index.d.ts +40 -3
  9. package/src/hooks/index.js +6 -6
  10. package/src/interface/aggregations.js +24 -0
  11. package/src/interface/aggregations.spec.js +91 -0
  12. package/src/interface/workflow.d.ts +12 -1
  13. package/src/interface/workflow.js +44 -20
  14. package/src/interface/workflow.spec.js +183 -7
  15. package/src/interface/workflow_context.js +4 -2
  16. package/src/tracing/processors/local/index.js +10 -4
  17. package/src/tracing/processors/local/index.spec.js +52 -21
  18. package/src/tracing/processors/s3/index.js +3 -3
  19. package/src/tracing/processors/s3/index.spec.js +26 -1
  20. package/src/tracing/processors/s3/s3_client.js +11 -3
  21. package/src/tracing/processors/s3/s3_client.spec.js +27 -15
  22. package/src/tracing/tools/build_trace_tree.js +1 -1
  23. package/src/tracing/tools/build_trace_tree.spec.js +49 -11
  24. package/src/tracing/tools/utils.js +0 -28
  25. package/src/tracing/tools/utils.spec.js +2 -134
  26. package/src/tracing/trace_attribute.d.ts +38 -0
  27. package/src/tracing/trace_attribute.js +80 -0
  28. package/src/tracing/trace_engine.js +12 -2
  29. package/src/worker/index.js +1 -1
  30. package/src/worker/index.spec.js +1 -1
  31. package/src/worker/interceptors/activity.js +9 -2
  32. package/src/worker/interceptors/activity.spec.js +16 -3
  33. package/src/worker/interceptors.js +2 -2
  34. package/src/worker/sinks.js +6 -6
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { EventAction } from '../trace_consts.js';
3
+ import { Attribute } from '#trace_attribute';
3
4
  import buildTraceTree from './build_trace_tree.js';
4
5
 
5
6
  describe( 'build_trace_tree', () => {
@@ -26,34 +27,66 @@ this can indicate it timed out or was interrupted.>>' );
26
27
  expect( buildTraceTree( entries ) ).toBeNull();
27
28
  } );
28
29
 
29
- it( 'add_attr action merges details.name and details.value into node.attributes', () => {
30
+ it( 'add_attr action stores attribute details by type on node.attributes', () => {
31
+ const requestCount = {
32
+ type: Attribute.HTTPRequestCount.TYPE,
33
+ url: 'https://api.example.test',
34
+ requestId: 'req-1'
35
+ };
36
+ const requestCost = {
37
+ type: Attribute.HTTPRequestCost.TYPE,
38
+ url: 'https://api.example.test',
39
+ requestId: 'req-1',
40
+ total: 0.2
41
+ };
30
42
  const entries = [
31
43
  { kind: 'workflow', id: 'wf', parentId: undefined, action: EventAction.START, name: 'wf', details: {}, timestamp: 100 },
32
44
  { kind: 'step', id: 's', parentId: 'wf', action: EventAction.START, name: 'step', details: {}, timestamp: 200 },
33
- { id: 's', action: EventAction.ADD_ATTR, details: { name: 'latency_ms', value: 42 }, timestamp: 250 },
34
- { id: 's', action: EventAction.ADD_ATTR, details: { name: 'retries', value: 1 }, timestamp: 260 },
45
+ { id: 's', action: EventAction.ADD_ATTR, details: requestCount, timestamp: 250 },
46
+ { id: 's', action: EventAction.ADD_ATTR, details: requestCost, timestamp: 260 },
35
47
  { id: 'wf', action: EventAction.END, details: {}, timestamp: 300 }
36
48
  ];
37
49
  const result = buildTraceTree( entries );
38
50
  expect( result ).not.toBeNull();
39
- expect( result.children[0].attributes ).toEqual( { latency_ms: 42, retries: 1 } );
51
+ expect( result.children[0].attributes ).toEqual( {
52
+ [Attribute.HTTPRequestCount.TYPE]: requestCount,
53
+ [Attribute.HTTPRequestCost.TYPE]: requestCost
54
+ } );
40
55
  } );
41
56
 
42
- it( 'add_attr action overwrites prior value for the same attribute name', () => {
57
+ it( 'add_attr action overwrites prior value for the same attribute type', () => {
58
+ const firstCost = {
59
+ type: Attribute.HTTPRequestCost.TYPE,
60
+ url: 'https://api.example.test',
61
+ requestId: 'req-1',
62
+ total: 1
63
+ };
64
+ const secondCost = {
65
+ type: Attribute.HTTPRequestCost.TYPE,
66
+ url: 'https://api.example.test',
67
+ requestId: 'req-1',
68
+ total: 2
69
+ };
43
70
  const entries = [
44
71
  { kind: 'workflow', id: 'wf', parentId: undefined, action: EventAction.START, name: 'wf', details: {}, timestamp: 1 },
45
- { id: 'wf', action: EventAction.ADD_ATTR, details: { name: 'x', value: 1 }, timestamp: 2 },
46
- { id: 'wf', action: EventAction.ADD_ATTR, details: { name: 'x', value: 2 }, timestamp: 3 },
72
+ { id: 'wf', action: EventAction.ADD_ATTR, details: firstCost, timestamp: 2 },
73
+ { id: 'wf', action: EventAction.ADD_ATTR, details: secondCost, timestamp: 3 },
47
74
  { id: 'wf', action: EventAction.END, details: {}, timestamp: 4 }
48
75
  ];
49
76
  const result = buildTraceTree( entries );
50
- expect( result.attributes ).toEqual( { x: 2 } );
77
+ expect( result.attributes ).toEqual( { [Attribute.HTTPRequestCost.TYPE]: secondCost } );
51
78
  } );
52
79
 
53
80
  it( 'add_attr does not attach nodes as children (only start does)', () => {
54
81
  const entries = [
55
82
  { kind: 'workflow', id: 'wf', parentId: undefined, action: EventAction.START, name: 'wf', details: {}, timestamp: 1 },
56
- { id: 'orphan', parentId: 'wf', action: EventAction.ADD_ATTR, details: { name: 'k', value: 'v' }, timestamp: 2 },
83
+ {
84
+ id: 'orphan',
85
+ parentId: 'wf',
86
+ action: EventAction.ADD_ATTR,
87
+ details: { type: Attribute.HTTPRequestCount.TYPE, url: 'https://api.example.test', requestId: 'req-1' },
88
+ timestamp: 2
89
+ },
57
90
  { id: 'wf', action: EventAction.END, details: {}, timestamp: 3 }
58
91
  ];
59
92
  const result = buildTraceTree( entries );
@@ -75,6 +108,11 @@ this can indicate it timed out or was interrupted.>>' );
75
108
  } );
76
109
 
77
110
  it( 'builds a tree from workflow/step/IO entries with grouping and sorting', () => {
111
+ const stepAttribute = {
112
+ type: Attribute.HTTPRequestCount.TYPE,
113
+ url: 'https://api.example.test/step-1',
114
+ requestId: 'req-step-1'
115
+ };
78
116
  const entries = [
79
117
  // workflow start
80
118
  { kind: 'workflow', action: EventAction.START, name: 'wf', id: 'wf', parentId: undefined, details: { a: 1 }, timestamp: 1000 },
@@ -83,7 +121,7 @@ this can indicate it timed out or was interrupted.>>' );
83
121
  { id: 'eval', action: EventAction.END, details: { z: 1 }, timestamp: 1600 },
84
122
  // step1 start
85
123
  { kind: 'step', action: EventAction.START, name: 'step-1', id: 's1', parentId: 'wf', details: { x: 1 }, timestamp: 2000 },
86
- { id: 's1', action: EventAction.ADD_ATTR, details: { name: 'step_tag', value: 'alpha' }, timestamp: 2050 },
124
+ { id: 's1', action: EventAction.ADD_ATTR, details: stepAttribute, timestamp: 2050 },
87
125
  // IO under step1
88
126
  { kind: 'IO', action: EventAction.START, name: 'test-1', id: 'io1', parentId: 's1', details: { y: 2 }, timestamp: 2300 },
89
127
  // step2 start
@@ -132,7 +170,7 @@ this can indicate it timed out or was interrupted.>>' );
132
170
  endedAt: 2800,
133
171
  input: { x: 1 },
134
172
  output: { done: true },
135
- attributes: { step_tag: 'alpha' },
173
+ attributes: { [Attribute.HTTPRequestCount.TYPE]: stepAttribute },
136
174
  children: [
137
175
  {
138
176
  id: 'io1',
@@ -19,31 +19,3 @@ export const serializeError = error =>
19
19
  message: error.message,
20
20
  stack: error.stack
21
21
  };
22
-
23
- /**
24
- * Tries to stringify an object to an indented JSON string.
25
- * If its byte size is bigger than threshold returns a plain JSON string without formatting.
26
- *
27
- * @param {object|array} content
28
- * @param {*} [threshold] - The max allowed size to try to stringify with formatting (in bytes). Default is 50mb
29
- * @returns {string} String representation of the object
30
- */
31
- export const safeFormatJSON = ( content, threshold = 50 * 1024 * 1024 /* 50mb */ ) => {
32
- const plainString = JSON.stringify( content );
33
- const plainStringSize = Buffer.byteLength( plainString, 'utf8' );
34
-
35
- if ( plainStringSize > threshold ) {
36
- return plainString;
37
- }
38
- try {
39
- return JSON.stringify( content, undefined, 2 );
40
- } catch ( error ) {
41
- // Only handles this specific error because other common parsing errors like:
42
- // "TypeError: cyclic object value" and "RangeError: Maximum call stack size exceeded"
43
- // would have been thrown on the first parsing.
44
- if ( error instanceof RangeError && error.message === 'Invalid string length' ) {
45
- return plainString;
46
- }
47
- throw error;
48
- }
49
- };
@@ -1,15 +1,5 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { safeFormatJSON, serializeError } from './utils.js';
3
-
4
- const isPrettyStringifyCall = args => args.length >= 3 && args[2] === 2;
5
-
6
- /** @param {number} targetBytes UTF-8 size of compact JSON.stringify( { a: "<xs>" } ) */
7
- const objectWithCompactByteLength = targetBytes => {
8
- const sample = { a: '' };
9
- const overhead = Buffer.byteLength( JSON.stringify( sample ), 'utf8' );
10
- const repeat = Math.max( 0, targetBytes - overhead );
11
- return { a: 'x'.repeat( repeat ) };
12
- };
1
+ import { describe, it, expect } from 'vitest';
2
+ import { serializeError } from './utils.js';
13
3
 
14
4
  describe( 'tracing/utils', () => {
15
5
  it( 'serializeError unwraps causes and keeps message/stack', () => {
@@ -21,126 +11,4 @@ describe( 'tracing/utils', () => {
21
11
  expect( out.message ).toBe( 'inner' );
22
12
  expect( typeof out.stack ).toBe( 'string' );
23
13
  } );
24
-
25
- describe( 'safeFormatJSON', () => {
26
- it( 'formats small objects with indentation when under threshold', () => {
27
- const content = { a: 1, b: [ 2, 3 ] };
28
- const out = safeFormatJSON( content, 10_000 );
29
-
30
- expect( out ).toContain( '\n' );
31
- expect( out ).toMatch( /^\{\n/ );
32
- expect( JSON.parse( out ) ).toEqual( content );
33
- } );
34
-
35
- it( 'formats small arrays with indentation when under threshold', () => {
36
- const content = [ 1, { nested: true } ];
37
- const out = safeFormatJSON( content, 10_000 );
38
-
39
- expect( out ).toContain( '\n' );
40
- expect( out.trimStart() ).toMatch( /^\[/ );
41
- expect( JSON.parse( out ) ).toEqual( content );
42
- } );
43
-
44
- it( 'returns compact JSON when compact UTF-8 size is strictly greater than threshold', () => {
45
- const content = objectWithCompactByteLength( 40 );
46
- const compact = JSON.stringify( content );
47
- expect( Buffer.byteLength( compact, 'utf8' ) ).toBe( 40 );
48
-
49
- const out = safeFormatJSON( content, 39 );
50
- expect( out ).toBe( compact );
51
- expect( out ).not.toContain( '\n ' );
52
- expect( JSON.parse( out ) ).toEqual( content );
53
- } );
54
-
55
- it( 'uses pretty JSON when compact UTF-8 size equals threshold', () => {
56
- const content = objectWithCompactByteLength( 40 );
57
- const compact = JSON.stringify( content );
58
- expect( Buffer.byteLength( compact, 'utf8' ) ).toBe( 40 );
59
-
60
- const out = safeFormatJSON( content, 40 );
61
- expect( out ).not.toBe( compact );
62
- expect( out ).toContain( '\n' );
63
- expect( JSON.parse( out ) ).toEqual( content );
64
- } );
65
-
66
- it( 'uses UTF-8 byte length for threshold, not JavaScript string length', () => {
67
- const content = { label: 'éclair' };
68
- const compact = JSON.stringify( content );
69
- expect( compact.length ).toBeLessThan( Buffer.byteLength( compact, 'utf8' ) );
70
-
71
- const bytes = Buffer.byteLength( compact, 'utf8' );
72
- const outCompact = safeFormatJSON( content, bytes - 1 );
73
- expect( outCompact ).toBe( compact );
74
-
75
- const outPretty = safeFormatJSON( content, bytes + 100 );
76
- expect( outPretty ).toContain( '\n' );
77
- expect( JSON.parse( outPretty ) ).toEqual( content );
78
- } );
79
-
80
- it( 'round-trips empty object and primitives for both branches', () => {
81
- const tiny = {};
82
- const pretty = safeFormatJSON( tiny, 100 );
83
- expect( JSON.parse( pretty ) ).toEqual( tiny );
84
-
85
- const forcedCompact = safeFormatJSON( tiny, 0 );
86
- expect( JSON.parse( forcedCompact ) ).toEqual( tiny );
87
- } );
88
-
89
- it( 'returns compact JSON when pretty stringify throws Invalid string length', () => {
90
- const content = { a: 1 };
91
- const compact = JSON.stringify( content );
92
- const origStringify = JSON.stringify.bind( JSON );
93
-
94
- const spy = vi.spyOn( JSON, 'stringify' ).mockImplementation( ( ...args ) => {
95
- if ( isPrettyStringifyCall( args ) ) {
96
- throw new RangeError( 'Invalid string length' );
97
- }
98
- return origStringify( ...args );
99
- } );
100
-
101
- try {
102
- const out = safeFormatJSON( content, 10_000 );
103
- expect( out ).toBe( compact );
104
- expect( JSON.parse( out ) ).toEqual( content );
105
- } finally {
106
- spy.mockRestore();
107
- }
108
- } );
109
-
110
- it( 'rethrows RangeError when message is not Invalid string length', () => {
111
- const content = { a: 1 };
112
- const origStringify = JSON.stringify.bind( JSON );
113
-
114
- const spy = vi.spyOn( JSON, 'stringify' ).mockImplementation( ( ...args ) => {
115
- if ( isPrettyStringifyCall( args ) ) {
116
- throw new RangeError( 'not the string length error' );
117
- }
118
- return origStringify( ...args );
119
- } );
120
-
121
- try {
122
- expect( () => safeFormatJSON( content, 10_000 ) ).toThrow( RangeError );
123
- } finally {
124
- spy.mockRestore();
125
- }
126
- } );
127
-
128
- it( 'rethrows non-RangeError from pretty stringify', () => {
129
- const content = { a: 1 };
130
- const origStringify = JSON.stringify.bind( JSON );
131
-
132
- const spy = vi.spyOn( JSON, 'stringify' ).mockImplementation( ( ...args ) => {
133
- if ( isPrettyStringifyCall( args ) ) {
134
- throw new TypeError( 'cyclic structure' );
135
- }
136
- return origStringify( ...args );
137
- } );
138
-
139
- try {
140
- expect( () => safeFormatJSON( content, 10_000 ) ).toThrow( TypeError );
141
- } finally {
142
- spy.mockRestore();
143
- }
144
- } );
145
- } );
146
14
  } );
@@ -0,0 +1,38 @@
1
+ export declare namespace Attribute {
2
+ export interface Usage {
3
+ type: string;
4
+ ppm: number;
5
+ amount: number;
6
+ total: number;
7
+ }
8
+
9
+ export class HTTPRequestCount {
10
+ static TYPE: 'http:request:count';
11
+ type: typeof HTTPRequestCount.TYPE;
12
+ url: string;
13
+ requestId: string;
14
+ constructor( url: string, requestId: string );
15
+ }
16
+
17
+ export class HTTPRequestCost {
18
+ static TYPE: 'http:request:cost';
19
+ type: typeof HTTPRequestCost.TYPE;
20
+ url: string;
21
+ requestId: string;
22
+ total: number;
23
+ constructor( url: string, requestId: string, total: number );
24
+ }
25
+
26
+ export class LLMUsage {
27
+ static TYPE: 'llm:usage';
28
+ type: typeof LLMUsage.TYPE;
29
+ modelId: string;
30
+ usage: Usage[];
31
+ constructor( modelId: string );
32
+ addUsage( usage: { type: string; ppm: number; amount: number } ): void;
33
+ readonly total: number;
34
+ readonly tokensUsed: number;
35
+ }
36
+
37
+ export type Instance = HTTPRequestCount | HTTPRequestCost | LLMUsage;
38
+ }
@@ -0,0 +1,80 @@
1
+ import Decimal from 'decimal.js';
2
+
3
+ /**
4
+ * All attributes inherit from this
5
+ */
6
+ export class BaseAttribute {
7
+ activityId;
8
+ activityName;
9
+ date = Date.now();
10
+ type;
11
+
12
+ constructor( type ) {
13
+ this.type = type;
14
+ }
15
+
16
+ setActivity( id, name ) {
17
+ this.activityId = id;
18
+ this.activityName = name;
19
+ }
20
+ }
21
+
22
+ class HTTPRequestCount extends BaseAttribute {
23
+ static TYPE = 'http:request:count';
24
+ url;
25
+ requestId;
26
+
27
+ constructor( url, requestId ) {
28
+ super( HTTPRequestCount.TYPE );
29
+ this.url = url;
30
+ this.requestId = requestId;
31
+ }
32
+ }
33
+
34
+ class HTTPRequestCost extends BaseAttribute {
35
+ static TYPE = 'http:request:cost';
36
+ url;
37
+ requestId;
38
+ total = 0;
39
+
40
+ constructor( url, requestId, total ) {
41
+ super( HTTPRequestCost.TYPE );
42
+ this.url = url;
43
+ this.requestId = requestId;
44
+ this.total = total;
45
+ }
46
+ }
47
+
48
+ class LLMUsage extends BaseAttribute {
49
+ static TYPE = 'llm:usage';
50
+ modelId;
51
+ usage = [];
52
+ total = 0;
53
+ tokensUsed = 0;
54
+
55
+ constructor( modelId ) {
56
+ super( LLMUsage.TYPE );
57
+ this.modelId = modelId;
58
+ }
59
+
60
+ addUsage( { type, ppm, amount } ) {
61
+ const total = Decimal( amount ).div( 1_000_000 ).mul( ppm ).toNumber();
62
+ this.usage.push( {
63
+ type,
64
+ ppm,
65
+ amount,
66
+ total
67
+ } );
68
+ this.total = Decimal( this.total ).add( total ).toNumber();
69
+ this.tokensUsed = Decimal( this.tokensUsed ).add( amount ).toNumber();
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Types of ADD_ATTR attributes
75
+ */
76
+ export const Attribute = {
77
+ LLMUsage,
78
+ HTTPRequestCost,
79
+ HTTPRequestCount
80
+ };
@@ -4,8 +4,10 @@ import { serializeError } from './tools/utils.js';
4
4
  import { isStringboolTrue } from '#utils';
5
5
  import * as localProcessor from './processors/local/index.js';
6
6
  import * as s3Processor from './processors/s3/index.js';
7
- import { ComponentType } from '#consts';
7
+ import { ComponentType, Signal } from '#consts';
8
8
  import { createChildLogger } from '#logger';
9
+ import { EventAction } from './trace_consts.js';
10
+ import { BaseAttribute } from './trace_attribute.js';
9
11
 
10
12
  const log = createChildLogger( 'Tracing' );
11
13
 
@@ -91,7 +93,15 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, exe
91
93
  export function addEventActionWithContext( action, options ) {
92
94
  const storeContent = Storage.load();
93
95
  if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
94
- const { parentId, executionContext } = storeContent;
96
+ const { parentId, parentName, executionContext, workflowHandle } = storeContent;
97
+ if ( action === EventAction.ADD_ATTR ) {
98
+ const attribute = options.details;
99
+ if ( !( attribute instanceof BaseAttribute ) ) {
100
+ throw new Error( `${EventAction.ADD_ATTR} called argument that is not a BaseAttribute instance` );
101
+ }
102
+ attribute.setActivity( parentId, parentName );
103
+ workflowHandle.signal( Signal.ADD_ATTRIBUTE, attribute );
104
+ }
95
105
  addEventAction( action, { ...options, parentId, executionContext } );
96
106
  }
97
107
  };
@@ -69,7 +69,7 @@ const callerDir = process.argv[2];
69
69
  workflowsPath,
70
70
  activities,
71
71
  sinks,
72
- interceptors: initInterceptors( { activities, workflows } ),
72
+ interceptors: initInterceptors( { activities, workflows, connection } ),
73
73
  maxConcurrentWorkflowTaskExecutions,
74
74
  maxConcurrentActivityTaskExecutions,
75
75
  maxCachedWorkflows,
@@ -123,7 +123,7 @@ describe( 'worker/index', () => {
123
123
  maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
124
124
  maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
125
125
  } ) );
126
- expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
126
+ expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [], connection: mockConnection } );
127
127
  expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
128
128
  expect( startCatalogMock ).toHaveBeenCalledWith( {
129
129
  connection: mockConnection,
@@ -5,6 +5,7 @@ import { headersToObject } from '../sandboxed_utils.js';
5
5
  import { BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
6
  import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
7
7
  import { messageBus } from '#bus';
8
+ import { Client } from '@temporalio/client';
8
9
 
9
10
  /*
10
11
  This interceptor wraps every activity execution with cross-cutting concerns:
@@ -23,7 +24,7 @@ import { messageBus } from '#bus';
23
24
  - Headers injected by the workflow interceptor (executionContext)
24
25
  */
25
26
  export class ActivityExecutionInterceptor {
26
- constructor( { activities, workflows } ) {
27
+ constructor( { activities, workflows, connection } ) {
27
28
  this.activities = activities;
28
29
  this.workflowsMap = workflows.reduce( ( map, w ) => {
29
30
  map.set( w.name, w );
@@ -32,14 +33,19 @@ export class ActivityExecutionInterceptor {
32
33
  }
33
34
  return map;
34
35
  }, new Map() );
36
+ this.connection = connection;
35
37
  };
36
38
 
37
39
  async execute( input, next ) {
38
40
  const startDate = Date.now();
41
+ const client = new Client( { connection: this.connection } );
42
+
39
43
  const { workflowExecution: { workflowId }, activityId: id, activityType: name, workflowType: workflowName } = Context.current().info;
40
44
  const { executionContext } = headersToObject( input.headers );
41
45
  const { type: kind } = this.activities?.[name]?.[METADATA_ACCESS_SYMBOL];
42
46
 
47
+ const workflowHandle = client.workflow.getHandle( workflowId );
48
+
43
49
  messageBus.emit( BusEventType.ACTIVITY_START, { id, name, kind, workflowId, workflowName } );
44
50
  Tracing.addEventStart( { id, name, kind, parentId: workflowId, details: input.args[0], executionContext } );
45
51
 
@@ -56,7 +62,8 @@ export class ActivityExecutionInterceptor {
56
62
  intervals.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
57
63
 
58
64
  // Wraps the execution with accessible metadata for the activity
59
- const output = await Storage.runWithContext( async _ => next( input ), { parentId: id, executionContext, workflowFilename } );
65
+ const ctx = { parentId: id, parentName: name, executionContext, workflowFilename, workflowHandle };
66
+ const output = await Storage.runWithContext( async _ => next( input ), ctx );
60
67
 
61
68
  messageBus.emit( BusEventType.ACTIVITY_END, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate } );
62
69
  Tracing.addEventEnd( { id, details: output, executionContext } );
@@ -2,6 +2,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { BusEventType } from '#consts';
3
3
 
4
4
  const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
5
+ const workflowHandleMock = vi.hoisted( () => ( { signal: vi.fn() } ) );
6
+ const getHandleMock = vi.hoisted( () => vi.fn( () => workflowHandleMock ) );
5
7
 
6
8
  const heartbeatMock = vi.fn();
7
9
  const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
@@ -21,6 +23,14 @@ vi.mock( '@temporalio/activity', () => ( {
21
23
  }
22
24
  } ) );
23
25
 
26
+ vi.mock( '@temporalio/client', () => ( {
27
+ Client: class Client {
28
+ workflow = {
29
+ getHandle: getHandleMock
30
+ };
31
+ }
32
+ } ) );
33
+
24
34
  vi.mock( '#async_storage', () => ( {
25
35
  Storage: {
26
36
  runWithContext: runWithContextMock
@@ -108,12 +118,15 @@ describe( 'ActivityExecutionInterceptor', () => {
108
118
  expect( addEventErrorMock ).not.toHaveBeenCalled();
109
119
  expect( runWithContextMock ).toHaveBeenCalledWith(
110
120
  expect.any( Function ),
111
- {
121
+ expect.objectContaining( {
112
122
  parentId: 'act-1',
123
+ parentName: 'myWorkflow#myStep',
113
124
  executionContext: { workflowId: 'wf-1' },
114
- workflowFilename: '/workflows/myWorkflow.js'
115
- }
125
+ workflowFilename: '/workflows/myWorkflow.js',
126
+ workflowHandle: workflowHandleMock
127
+ } )
116
128
  );
129
+ expect( getHandleMock ).toHaveBeenCalledWith( 'wf-1' );
117
130
  } );
118
131
 
119
132
  it( 'records trace error event on failed execution', async () => {
@@ -4,7 +4,7 @@ import { ActivityExecutionInterceptor } from './interceptors/activity.js';
4
4
 
5
5
  const __dirname = dirname( fileURLToPath( import.meta.url ) );
6
6
 
7
- export const initInterceptors = ( { activities, workflows } ) => ( {
7
+ export const initInterceptors = ( { activities, workflows, connection } ) => ( {
8
8
  workflowModules: [ join( __dirname, './interceptors/workflow.js' ) ],
9
- activityInbound: [ () => new ActivityExecutionInterceptor( { activities, workflows } ) ]
9
+ activityInbound: [ () => new ActivityExecutionInterceptor( { activities, workflows, connection } ) ]
10
10
  } );
@@ -11,8 +11,8 @@ export const sinks = {
11
11
  workflow: {
12
12
  start: {
13
13
  fn: ( workflowInfo, input ) => {
14
- const { workflowId: id, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
15
- messageBus.emit( BusEventType.WORKFLOW_START, { id, name } );
14
+ const { workflowId: id, runId, workflowType: name, memo: { parentId, executionContext } } = workflowInfo;
15
+ messageBus.emit( BusEventType.WORKFLOW_START, { id, runId, name } );
16
16
  if ( executionContext ) { // filters out internal workflows
17
17
  Tracing.addEventStart( { id, kind: ComponentType.WORKFLOW, name, details: input, parentId, executionContext } );
18
18
  }
@@ -22,8 +22,8 @@ export const sinks = {
22
22
 
23
23
  end: {
24
24
  fn: ( workflowInfo, output ) => {
25
- const { workflowId: id, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
26
- messageBus.emit( BusEventType.WORKFLOW_END, { id, name, duration: Date.now() - startTime.getTime() } );
25
+ const { workflowId: id, runId, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
26
+ messageBus.emit( BusEventType.WORKFLOW_END, { id, runId, name, duration: Date.now() - startTime.getTime() } );
27
27
  if ( executionContext ) { // filters out internal workflows
28
28
  Tracing.addEventEnd( { id, details: output, executionContext } );
29
29
  }
@@ -33,8 +33,8 @@ export const sinks = {
33
33
 
34
34
  error: {
35
35
  fn: ( workflowInfo, error ) => {
36
- const { workflowId: id, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
37
- messageBus.emit( BusEventType.WORKFLOW_ERROR, { id, name, error, duration: Date.now() - startTime.getTime() } );
36
+ const { workflowId: id, runId, workflowType: name, startTime, memo: { executionContext } } = workflowInfo;
37
+ messageBus.emit( BusEventType.WORKFLOW_ERROR, { id, runId, name, error, duration: Date.now() - startTime.getTime() } );
38
38
  if ( executionContext ) { // filters out internal workflows
39
39
  Tracing.addEventError( { id, details: error, executionContext } );
40
40
  }