@outputai/core 0.4.0 → 0.4.1-dev.10cf346.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-dev.10cf346.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,6 +19,9 @@
19
19
  "./sdk_utils": {
20
20
  "types": "./src/utils/index.d.ts",
21
21
  "import": "./src/utils/index.js"
22
+ },
23
+ "./sdk_tracing_tools": {
24
+ "import": "./src/tracing/tools/index.js"
22
25
  }
23
26
  },
24
27
  "files": [
@@ -42,4 +42,5 @@ export declare function addEventAttribute( args: { eventId: string; name: string
42
42
  */
43
43
  export declare const Attribute: {
44
44
  COST: 'cost';
45
+ TOKEN_USAGE: 'token_usage';
45
46
  };
@@ -49,5 +49,6 @@ export const addEventAttribute = ( { eventId, name, value } ) =>
49
49
  * Known attributes
50
50
  */
51
51
  export const Attribute = {
52
- COST: 'cost'
52
+ COST: 'cost',
53
+ TOKEN_USAGE: 'token_usage'
53
54
  };
@@ -16,8 +16,10 @@ export interface ErrorHookPayload {
16
16
  * Payload passed to the onWorkflowStart handler when a workflow run begins.
17
17
  */
18
18
  export interface WorkflowStartHookPayload {
19
- /** Identifier of the workflow run. */
19
+ /** Workflow id (stable across retries / continue-as-new). */
20
20
  id: string;
21
+ /** Temporal run id for the current execution attempt. */
22
+ runId: string;
21
23
  /** Name of the workflow. */
22
24
  name: string;
23
25
  }
@@ -26,8 +28,10 @@ export interface WorkflowStartHookPayload {
26
28
  * Payload passed to the onWorkflowEnd handler when a workflow run completes successfully.
27
29
  */
28
30
  export interface WorkflowEndHookPayload {
29
- /** Identifier of the workflow run. */
31
+ /** Workflow id (stable across retries / continue-as-new). */
30
32
  id: string;
33
+ /** Temporal run id for the current execution attempt. */
34
+ runId: string;
31
35
  /** Name of the workflow. */
32
36
  name: string;
33
37
  /** Duration of the workflow run in milliseconds. */
@@ -38,8 +42,10 @@ export interface WorkflowEndHookPayload {
38
42
  * Payload passed to the onWorkflowError handler when a workflow run fails.
39
43
  */
40
44
  export interface WorkflowErrorHookPayload {
41
- /** Identifier of the workflow run. */
45
+ /** Workflow id (stable across retries / continue-as-new). */
42
46
  id: string;
47
+ /** Temporal run id for the current execution attempt. */
48
+ runId: string;
43
49
  /** Name of the workflow. */
44
50
  name: string;
45
51
  /** Elapsed time before failure in milliseconds. */
@@ -34,16 +34,16 @@ export const onBeforeWorkerStart = handler => messageBus.on( BusEventType.WORKER
34
34
  safeInvoke( handler, undefined, 'onBeforeWorkerStart' ) );
35
35
 
36
36
  /** Listen to workflow start events, excludes catalog workflow */
37
- export const onWorkflowStart = handler => messageBus.on( BusEventType.WORKFLOW_START, ( { id, name } ) =>
38
- WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, name }, 'onWorkflowStart' ) : null );
37
+ export const onWorkflowStart = handler => messageBus.on( BusEventType.WORKFLOW_START, ( { id, runId, name } ) =>
38
+ WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, runId, name }, 'onWorkflowStart' ) : null );
39
39
 
40
40
  /** Listen to workflow end events, excludes catalog workflow */
41
- export const onWorkflowEnd = handler => messageBus.on( BusEventType.WORKFLOW_END, ( { id, name, duration } ) =>
42
- WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, name, duration }, 'onWorkflowEnd' ) : null );
41
+ export const onWorkflowEnd = handler => messageBus.on( BusEventType.WORKFLOW_END, ( { id, runId, name, duration } ) =>
42
+ WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, runId, name, duration }, 'onWorkflowEnd' ) : null );
43
43
 
44
44
  /** Listen to workflow error events, excludes catalog workflow */
45
- export const onWorkflowError = handler => messageBus.on( BusEventType.WORKFLOW_ERROR, ( { id, name, duration, error } ) =>
46
- WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, name, duration, error }, 'onWorkflowError' ) : null );
45
+ export const onWorkflowError = handler => messageBus.on( BusEventType.WORKFLOW_ERROR, ( { id, runId, name, duration, error } ) =>
46
+ WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, runId, name, duration, error }, 'onWorkflowError' ) : null );
47
47
 
48
48
  /** Generic listener for events emitted elsewhere (outside core) */
49
49
  export const on = ( eventName, handler ) => messageBus.on( `external:${eventName}`, payload =>
@@ -63,7 +63,18 @@ export type WorkflowContext<
63
63
  *
64
64
  * @see {@link https://docs.temporal.io/workflow-execution/workflowid-runid#workflow-id}
65
65
  */
66
- workflowId: string
66
+ workflowId: string,
67
+
68
+ /**
69
+ * Internal Temporal run id for the current execution attempt.
70
+ *
71
+ * A single `workflowId` can map to multiple `runId`s when a workflow is
72
+ * retried, reset, or continued-as-new. The current run can be pinned in
73
+ * downstream `/workflow/{id}/runs/{rid}/...` API calls.
74
+ *
75
+ * @see {@link https://docs.temporal.io/workflow-execution/workflowid-runid#run-id}
76
+ */
77
+ runId: string
67
78
  }
68
79
  };
69
80
 
@@ -39,15 +39,20 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
39
39
  // this returns a plain function, for example, in unit tests
40
40
  if ( !inWorkflowContext() ) {
41
41
  validateWithSchema( inputSchema, input, `Workflow ${name} input` );
42
- const context = Context.build( { workflowId: 'test-workflow', continueAsNew: async () => {}, isContinueAsNewSuggested: () => false } );
42
+ const context = Context.build( {
43
+ workflowId: 'test-workflow',
44
+ runId: 'test-run',
45
+ continueAsNew: async () => {},
46
+ isContinueAsNewSuggested: () => false
47
+ } );
43
48
  const output = await fn( input, deepMerge( context, extra.context ) );
44
49
  validateWithSchema( outputSchema, output, `Workflow ${name} output` );
45
50
  return output;
46
51
  }
47
52
 
48
- const { workflowId, memo, startTime } = workflowInfo();
53
+ const { workflowId, runId, memo, startTime } = workflowInfo();
49
54
 
50
- const context = Context.build( { workflowId, continueAsNew, isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested } );
55
+ const context = Context.build( { workflowId, runId, continueAsNew, isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested } );
51
56
 
52
57
  // Root workflows will not have the execution context yet, since it is set here.
53
58
  const isRoot = !memo.executionContext;
@@ -57,6 +62,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
57
62
  It will be used to as context for tracing (connecting events) */
58
63
  const executionContext = memo.executionContext ?? {
59
64
  workflowId,
65
+ runId,
60
66
  workflowName: name,
61
67
  disableTrace,
62
68
  startTime: startTime.getTime()
@@ -7,11 +7,12 @@ export class Context {
7
7
  * Builds a new context instance
8
8
  * @param {object} options - Arguments to build a new context instance
9
9
  * @param {string} workflowId
10
+ * @param {string} runId
10
11
  * @param {function} continueAsNew
11
12
  * @param {function} isContinueAsNewSuggested
12
13
  * @returns {object} context
13
14
  */
14
- static build( { workflowId, continueAsNew, isContinueAsNewSuggested } ) {
15
+ static build( { workflowId, runId, continueAsNew, isContinueAsNewSuggested } ) {
15
16
  return {
16
17
  /**
17
18
  * Control namespace: This object adds functions to interact with Temporal flow mechanisms
@@ -24,7 +25,8 @@ export class Context {
24
25
  * Info namespace: abstracts workflowInfo()
25
26
  */
26
27
  info: {
27
- workflowId
28
+ workflowId,
29
+ runId
28
30
  }
29
31
  };
30
32
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Aggregate `attributes.cost` and `attributes.token_usage` across an entire trace tree.
3
+ *
4
+ * Walks every node in the tree, sums `attributes.cost.total` grouped by the emitting
5
+ * event name (inferred from node `kind` — see `eventNameForKind`), and sums
6
+ * `attributes.token_usage` across LLM nodes. Falls back to `output.usage` on
7
+ * legacy llm trace nodes that predate the `attributes.token_usage` write
8
+ * (see overview §1.2).
9
+ *
10
+ * @typedef {object} TraceAttributes
11
+ * @property {{ total: number, components: Array<{ name: string, value: number }> }} cost
12
+ * @property {{ inputTokens: number, outputTokens: number, cachedInputTokens: number, totalTokens: number }} tokenUsage
13
+ */
14
+
15
+ const COST_EVENT_LLM = 'cost:llm:request';
16
+ const COST_EVENT_HTTP = 'cost:http:request';
17
+ const COST_EVENT_OTHER = 'other';
18
+
19
+ /**
20
+ * Map a trace node `kind` to the canonical cost event name that would emit it.
21
+ * Unknown kinds bucket into `other` so future event sources still roll up cleanly.
22
+ *
23
+ * @param {string} kind
24
+ * @returns {string}
25
+ */
26
+ const eventNameForKind = kind => {
27
+ if ( kind === 'llm' ) {
28
+ return COST_EVENT_LLM;
29
+ }
30
+ if ( kind === 'http' ) {
31
+ return COST_EVENT_HTTP;
32
+ }
33
+ return COST_EVENT_OTHER;
34
+ };
35
+
36
+ const isNumber = value => typeof value === 'number' && Number.isFinite( value );
37
+
38
+ /**
39
+ * Pull token usage off an llm node, preferring the new attribute over the legacy
40
+ * `output.usage` fallback. Returns `null` when neither shape is present.
41
+ */
42
+ const readTokenUsage = node => {
43
+ const attrUsage = node.attributes?.token_usage;
44
+ if ( attrUsage && typeof attrUsage === 'object' ) {
45
+ return attrUsage;
46
+ }
47
+ const legacyUsage = node.output?.usage;
48
+ if ( legacyUsage && typeof legacyUsage === 'object' ) {
49
+ return legacyUsage;
50
+ }
51
+ return null;
52
+ };
53
+
54
+ /**
55
+ * Recursively walk a trace tree depth-first, applying `visit` to each node.
56
+ */
57
+ const walk = ( node, visit ) => {
58
+ if ( !node ) {
59
+ return;
60
+ }
61
+ visit( node );
62
+ for ( const child of node.children ?? [] ) {
63
+ walk( child, visit );
64
+ }
65
+ };
66
+
67
+ /**
68
+ * Build the aggregated `attributes` payload returned by `/trace-attributes`.
69
+ * Component buckets always appear in a stable order so callers can index them
70
+ * positionally if they want to.
71
+ *
72
+ * @param {object|null} root - The root NodeEntry returned by `buildTraceTree`.
73
+ * @returns {TraceAttributes}
74
+ */
75
+ export default function aggregateTraceAttributes( root ) {
76
+ const costByEvent = new Map( [
77
+ [ COST_EVENT_LLM, 0 ],
78
+ [ COST_EVENT_HTTP, 0 ],
79
+ [ COST_EVENT_OTHER, 0 ]
80
+ ] );
81
+ const tokenUsage = { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, totalTokens: 0 };
82
+
83
+ walk( root, node => {
84
+ const cost = node.attributes?.cost;
85
+ if ( cost && isNumber( cost.total ) ) {
86
+ const eventName = eventNameForKind( node.kind );
87
+ costByEvent.set( eventName, ( costByEvent.get( eventName ) ?? 0 ) + cost.total );
88
+ }
89
+
90
+ if ( node.kind === 'llm' ) {
91
+ const usage = readTokenUsage( node );
92
+ if ( usage ) {
93
+ if ( isNumber( usage.inputTokens ) ) {
94
+ tokenUsage.inputTokens += usage.inputTokens;
95
+ }
96
+ if ( isNumber( usage.outputTokens ) ) {
97
+ tokenUsage.outputTokens += usage.outputTokens;
98
+ }
99
+ if ( isNumber( usage.cachedInputTokens ) ) {
100
+ tokenUsage.cachedInputTokens += usage.cachedInputTokens;
101
+ }
102
+ if ( isNumber( usage.totalTokens ) ) {
103
+ tokenUsage.totalTokens += usage.totalTokens;
104
+ }
105
+ }
106
+ }
107
+ } );
108
+
109
+ const components = Array.from( costByEvent, ( [ name, value ] ) => ( { name, value } ) );
110
+ const total = components.reduce( ( sum, { value } ) => sum + value, 0 );
111
+
112
+ return {
113
+ cost: { total, components },
114
+ tokenUsage
115
+ };
116
+ }
117
+
118
+ export { COST_EVENT_LLM, COST_EVENT_HTTP, COST_EVENT_OTHER };
@@ -0,0 +1,231 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import aggregateTraceAttributes, {
3
+ COST_EVENT_LLM,
4
+ COST_EVENT_HTTP,
5
+ COST_EVENT_OTHER
6
+ } from './aggregate_trace_attributes.js';
7
+
8
+ const node = ( { id, kind = 'step', attributes = {}, output, children = [] } ) => ( {
9
+ id,
10
+ kind,
11
+ name: id,
12
+ startedAt: 0,
13
+ endedAt: 0,
14
+ input: undefined,
15
+ output,
16
+ attributes,
17
+ children
18
+ } );
19
+
20
+ describe( 'aggregate_trace_attributes', () => {
21
+ it( 'returns zeros for a null root', () => {
22
+ const result = aggregateTraceAttributes( null );
23
+ expect( result.cost.total ).toBe( 0 );
24
+ expect( result.cost.components ).toEqual( [
25
+ { name: COST_EVENT_LLM, value: 0 },
26
+ { name: COST_EVENT_HTTP, value: 0 },
27
+ { name: COST_EVENT_OTHER, value: 0 }
28
+ ] );
29
+ expect( result.tokenUsage ).toEqual( {
30
+ inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, totalTokens: 0
31
+ } );
32
+ } );
33
+
34
+ it( 'returns zeros for a tree with no cost or usage attributes', () => {
35
+ const root = node( {
36
+ id: 'wf',
37
+ kind: 'workflow',
38
+ children: [ node( { id: 's1' } ), node( { id: 's2' } ) ]
39
+ } );
40
+ const result = aggregateTraceAttributes( root );
41
+ expect( result.cost.total ).toBe( 0 );
42
+ expect( result.tokenUsage.totalTokens ).toBe( 0 );
43
+ } );
44
+
45
+ it( 'buckets cost by node kind into llm / http / other components', () => {
46
+ const root = node( {
47
+ id: 'wf',
48
+ kind: 'workflow',
49
+ children: [
50
+ node( { id: 'llm-1', kind: 'llm', attributes: { cost: { total: 0.20 } } } ),
51
+ node( { id: 'llm-2', kind: 'llm', attributes: { cost: { total: 0.10 } } } ),
52
+ node( { id: 'http-1', kind: 'http', attributes: { cost: { total: 0.50 } } } ),
53
+ // Unknown kind falls into the catch-all bucket
54
+ node( { id: 'step-1', kind: 'step', attributes: { cost: { total: 0.07 } } } )
55
+ ]
56
+ } );
57
+ const result = aggregateTraceAttributes( root );
58
+
59
+ const byName = Object.fromEntries( result.cost.components.map( c => [ c.name, c.value ] ) );
60
+ expect( byName[COST_EVENT_LLM] ).toBeCloseTo( 0.30, 10 );
61
+ expect( byName[COST_EVENT_HTTP] ).toBeCloseTo( 0.50, 10 );
62
+ expect( byName[COST_EVENT_OTHER] ).toBeCloseTo( 0.07, 10 );
63
+ expect( result.cost.total ).toBeCloseTo( 0.87, 10 );
64
+ } );
65
+
66
+ it( 'total equals the sum of all components', () => {
67
+ const root = node( {
68
+ id: 'wf',
69
+ kind: 'workflow',
70
+ children: [
71
+ node( { id: 'llm-1', kind: 'llm', attributes: { cost: { total: 0.1234 } } } ),
72
+ node( { id: 'http-1', kind: 'http', attributes: { cost: { total: 0.0011 } } } )
73
+ ]
74
+ } );
75
+ const { cost } = aggregateTraceAttributes( root );
76
+ const sum = cost.components.reduce( ( s, c ) => s + c.value, 0 );
77
+ expect( cost.total ).toBeCloseTo( sum, 10 );
78
+ } );
79
+
80
+ it( 'sums token_usage across llm nodes from the attribute path', () => {
81
+ const root = node( {
82
+ id: 'wf',
83
+ kind: 'workflow',
84
+ children: [
85
+ node( {
86
+ id: 'llm-1', kind: 'llm', attributes: {
87
+ token_usage: { inputTokens: 100, outputTokens: 20, cachedInputTokens: 5, totalTokens: 125 }
88
+ }
89
+ } ),
90
+ node( {
91
+ id: 'llm-2', kind: 'llm', attributes: {
92
+ token_usage: { inputTokens: 50, outputTokens: 10, cachedInputTokens: 1, totalTokens: 61 }
93
+ }
94
+ } )
95
+ ]
96
+ } );
97
+ const { tokenUsage } = aggregateTraceAttributes( root );
98
+ expect( tokenUsage ).toEqual( {
99
+ inputTokens: 150,
100
+ outputTokens: 30,
101
+ cachedInputTokens: 6,
102
+ totalTokens: 186
103
+ } );
104
+ } );
105
+
106
+ it( 'falls back to output.usage on legacy llm nodes that lack attributes.token_usage', () => {
107
+ const root = node( {
108
+ id: 'wf',
109
+ kind: 'workflow',
110
+ children: [
111
+ // Legacy shape — usage lives on output.usage, no attributes.token_usage
112
+ node( {
113
+ id: 'llm-legacy',
114
+ kind: 'llm',
115
+ output: { result: '...', usage: { inputTokens: 200, outputTokens: 40, totalTokens: 240 } }
116
+ } )
117
+ ]
118
+ } );
119
+ const { tokenUsage } = aggregateTraceAttributes( root );
120
+ expect( tokenUsage.inputTokens ).toBe( 200 );
121
+ expect( tokenUsage.outputTokens ).toBe( 40 );
122
+ expect( tokenUsage.totalTokens ).toBe( 240 );
123
+ expect( tokenUsage.cachedInputTokens ).toBe( 0 );
124
+ } );
125
+
126
+ it( 'prefers attributes.token_usage over output.usage when both are present', () => {
127
+ const root = node( {
128
+ id: 'wf',
129
+ kind: 'workflow',
130
+ children: [
131
+ node( {
132
+ id: 'llm-1',
133
+ kind: 'llm',
134
+ attributes: { token_usage: { inputTokens: 10, outputTokens: 2, totalTokens: 12 } },
135
+ output: { usage: { inputTokens: 999, outputTokens: 999, totalTokens: 999 } }
136
+ } )
137
+ ]
138
+ } );
139
+ const { tokenUsage } = aggregateTraceAttributes( root );
140
+ expect( tokenUsage.inputTokens ).toBe( 10 );
141
+ expect( tokenUsage.totalTokens ).toBe( 12 );
142
+ } );
143
+
144
+ it( 'ignores token_usage shapes on non-llm nodes', () => {
145
+ const root = node( {
146
+ id: 'wf',
147
+ kind: 'workflow',
148
+ // attributes.token_usage on a non-llm node is intentionally ignored —
149
+ // only llm nodes contribute to the token-usage rollup today.
150
+ children: [
151
+ node( {
152
+ id: 'step-1', kind: 'step', attributes: {
153
+ token_usage: { inputTokens: 999, outputTokens: 999, totalTokens: 999 }
154
+ }
155
+ } )
156
+ ]
157
+ } );
158
+ const { tokenUsage } = aggregateTraceAttributes( root );
159
+ expect( tokenUsage.totalTokens ).toBe( 0 );
160
+ } );
161
+
162
+ it( 'aggregates a mixed tree with cost on http nodes and usage on llm nodes', () => {
163
+ const root = node( {
164
+ id: 'wf',
165
+ kind: 'workflow',
166
+ children: [
167
+ node( {
168
+ id: 'llm-1',
169
+ kind: 'llm',
170
+ attributes: {
171
+ cost: { total: 0.0038 },
172
+ token_usage: { inputTokens: 2264, outputTokens: 411, cachedInputTokens: 100, totalTokens: 2775 }
173
+ }
174
+ } ),
175
+ node( {
176
+ id: 'http-1',
177
+ kind: 'http',
178
+ attributes: { cost: { total: 0.50 } }
179
+ } )
180
+ ]
181
+ } );
182
+ const result = aggregateTraceAttributes( root );
183
+
184
+ const byName = Object.fromEntries( result.cost.components.map( c => [ c.name, c.value ] ) );
185
+ expect( byName[COST_EVENT_LLM] ).toBeCloseTo( 0.0038, 10 );
186
+ expect( byName[COST_EVENT_HTTP] ).toBeCloseTo( 0.50, 10 );
187
+ expect( byName[COST_EVENT_OTHER] ).toBe( 0 );
188
+ expect( result.cost.total ).toBeCloseTo( 0.5038, 10 );
189
+
190
+ expect( result.tokenUsage ).toEqual( {
191
+ inputTokens: 2264,
192
+ outputTokens: 411,
193
+ cachedInputTokens: 100,
194
+ totalTokens: 2775
195
+ } );
196
+ } );
197
+
198
+ it( 'recurses through nested children', () => {
199
+ const root = node( {
200
+ id: 'wf',
201
+ kind: 'workflow',
202
+ children: [
203
+ node( {
204
+ id: 's1',
205
+ kind: 'step',
206
+ children: [
207
+ node( {
208
+ id: 'llm-1', kind: 'llm', attributes: {
209
+ cost: { total: 0.01 },
210
+ token_usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
211
+ }
212
+ } )
213
+ ]
214
+ } )
215
+ ]
216
+ } );
217
+ const result = aggregateTraceAttributes( root );
218
+ expect( result.cost.total ).toBeCloseTo( 0.01, 10 );
219
+ expect( result.tokenUsage.totalTokens ).toBe( 15 );
220
+ } );
221
+
222
+ it( 'keeps the canonical component ordering: llm, http, other', () => {
223
+ const root = node( { id: 'wf', kind: 'workflow' } );
224
+ const { cost } = aggregateTraceAttributes( root );
225
+ expect( cost.components.map( c => c.name ) ).toEqual( [
226
+ COST_EVENT_LLM,
227
+ COST_EVENT_HTTP,
228
+ COST_EVENT_OTHER
229
+ ] );
230
+ } );
231
+ } );
@@ -0,0 +1,7 @@
1
+ export { default as buildTraceTree } from './build_trace_tree.js';
2
+ export {
3
+ default as aggregateTraceAttributes,
4
+ COST_EVENT_LLM,
5
+ COST_EVENT_HTTP,
6
+ COST_EVENT_OTHER
7
+ } from './aggregate_trace_attributes.js';
@@ -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
  }