@outputai/core 0.5.3-next.0eeffec.0 → 0.5.3-next.bdf47aa.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 +3 -1
- package/src/consts.js +3 -1
- package/src/interface/workflow.js +110 -41
- package/src/interface/workflow.replay_compatibility.spec.js +254 -0
- package/src/interface/workflow.spec.js +78 -126
- package/src/internal_utils/aggregations.js +54 -0
- package/src/{interface → internal_utils}/aggregations.spec.js +49 -1
- package/src/internal_utils/errors.js +10 -0
- package/src/tracing/trace_attribute.js +0 -7
- package/src/tracing/trace_engine.js +2 -2
- package/src/tracing/trace_engine.spec.js +8 -8
- package/src/utils/index.d.ts +15 -1
- package/src/utils/utils.js +38 -7
- package/src/utils/utils.spec.js +132 -0
- package/src/worker/catalog_workflow/workflow.js +4 -1
- package/src/worker/configs.js +0 -6
- package/src/worker/configs.spec.js +1 -27
- package/src/worker/index.js +5 -1
- package/src/worker/index.spec.js +6 -1
- package/src/worker/interceptors/activity.js +31 -48
- package/src/worker/interceptors/activity.spec.js +67 -59
- package/src/worker/interceptors.js +5 -1
- package/src/worker/loader_tools.js +25 -0
- package/src/worker/start_catalog.js +78 -18
- package/src/worker/start_catalog.spec.js +75 -14
- package/src/interface/aggregations.js +0 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.5.3-next.
|
|
3
|
+
"version": "0.5.3-next.bdf47aa.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@temporalio/worker": "1.17.0",
|
|
45
45
|
"@temporalio/workflow": "1.17.0",
|
|
46
46
|
"decimal.js": "10.6.0",
|
|
47
|
+
"folder-hash": "4.1.3",
|
|
47
48
|
"json-stream-stringify": "3.1.6",
|
|
48
49
|
"redis": "5.12.1",
|
|
49
50
|
"stacktrace-parser": "0.1.11",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"#errors": "./src/errors.js",
|
|
67
68
|
"#logger": "./src/logger.js",
|
|
68
69
|
"#utils": "./src/utils/index.js",
|
|
70
|
+
"#internal_utils/*": "./src/internal_utils/*.js",
|
|
69
71
|
"#tracing": "./src/tracing/internal_interface.js",
|
|
70
72
|
"#trace_attribute": "./src/tracing/trace_attribute.js",
|
|
71
73
|
"#async_storage": "./src/async_storage.js",
|
package/src/consts.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export const ACTIVITY_GET_TRACE_DESTINATIONS = '__internal#getTraceDestinations';
|
|
2
2
|
export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
|
|
3
3
|
export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
|
|
4
|
+
export const ACTIVITY_WRAPPER_VERSION_FIELD = '__output_activity_wrapper_version';
|
|
4
5
|
export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
|
|
5
6
|
export const SHARED_STEP_PREFIX = '$shared';
|
|
6
7
|
export const WORKFLOW_CATALOG = '$catalog';
|
|
7
8
|
export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
|
|
9
|
+
export const WORKFLOW_WRAPPER_VERSION_FIELD = '__output_workflow_wrapper_version';
|
|
8
10
|
|
|
9
11
|
export const ComponentType = {
|
|
10
12
|
EVALUATOR: 'evaluator',
|
|
@@ -34,7 +36,7 @@ export const BusEventType = {
|
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
export const Signal = {
|
|
37
|
-
|
|
39
|
+
SEND_AGGREGATIONS: 'send_aggregations'
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
export const WorkflowSpecialOutput = {
|
|
@@ -3,11 +3,19 @@ import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4,
|
|
|
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 {
|
|
6
|
+
import {
|
|
7
|
+
ACTIVITY_GET_TRACE_DESTINATIONS,
|
|
8
|
+
ACTIVITY_WRAPPER_VERSION_FIELD,
|
|
9
|
+
METADATA_ACCESS_SYMBOL,
|
|
10
|
+
SHARED_STEP_PREFIX,
|
|
11
|
+
Signal,
|
|
12
|
+
WORKFLOW_WRAPPER_VERSION_FIELD
|
|
13
|
+
} from '#consts';
|
|
7
14
|
import { deepMerge, setMetadata, toUrlSafeBase64 } from '#utils';
|
|
8
15
|
import { FatalError, ValidationError } from '#errors';
|
|
9
16
|
import { Context } from './workflow_context.js';
|
|
10
|
-
import { aggregateAttributes } from '
|
|
17
|
+
import { aggregateAttributes, mergeAggregations } from '#internal_utils/aggregations';
|
|
18
|
+
import { extractErrorDetail } from '#internal_utils/errors';
|
|
11
19
|
|
|
12
20
|
const defaultOptions = {
|
|
13
21
|
activityOptions: {
|
|
@@ -24,8 +32,10 @@ const defaultOptions = {
|
|
|
24
32
|
disableTrace: false
|
|
25
33
|
};
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the activity result uses the internal wrapper
|
|
37
|
+
*/
|
|
38
|
+
const isActivityResultWrapped = result => result?.[ACTIVITY_WRAPPER_VERSION_FIELD] > 0;
|
|
29
39
|
|
|
30
40
|
export function workflow( { name, description, inputSchema, outputSchema, fn, options = {}, aliases = [] } ) {
|
|
31
41
|
validateWorkflow( { name, description, inputSchema, outputSchema, fn, options, aliases } );
|
|
@@ -56,7 +66,6 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
const { workflowId, runId, memo, startTime } = workflowInfo();
|
|
59
|
-
|
|
60
69
|
const context = Context.build( { workflowId, runId, continueAsNew, isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested } );
|
|
61
70
|
|
|
62
71
|
// Root workflows will not have the execution context yet, since it is set here.
|
|
@@ -78,32 +87,82 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
78
87
|
activityOptions: memo.activityOptions ?? activityOptions // Also preserve the original activity options
|
|
79
88
|
} );
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Run the internal activity to retrieve the workflow trace destinations
|
|
92
|
+
* This only happens at the root workflow because nested share the same trace file
|
|
93
|
+
* @IMPORTANT Keep support for deprecated non-wrapped activity result to allow for Temporal replays.
|
|
94
|
+
* @TODO [OUT-468]
|
|
95
|
+
*/
|
|
96
|
+
const getTraceDestinations = async () => {
|
|
97
|
+
const result = await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( executionContext );
|
|
98
|
+
return isActivityResultWrapped( result ) ? result.output : result;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Creates the result wrapper with information about the workflow
|
|
102
|
+
const workflowResult = {
|
|
103
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
104
|
+
aggregations: null,
|
|
105
|
+
...( isRoot && {
|
|
106
|
+
trace: {
|
|
107
|
+
destinations: await getTraceDestinations()
|
|
108
|
+
}
|
|
109
|
+
} )
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Combine aggregations in the workflow result aggregations, mutating it
|
|
113
|
+
const mergeAggregationsInWorkflowResult = aggregations => {
|
|
114
|
+
workflowResult.aggregations = mergeAggregations( workflowResult.aggregations, aggregations );
|
|
115
|
+
};
|
|
83
116
|
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
setHandler( defineSignal( Signal.SEND_AGGREGATIONS ), aggregations => {
|
|
118
|
+
mergeAggregationsInWorkflowResult( aggregations );
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @IMPORTANT Keep support for deprecated add_attribute Signal to allow for Temporal replays.
|
|
123
|
+
* @TODO This can be removed 30days after this release
|
|
124
|
+
*/
|
|
125
|
+
setHandler( defineSignal( 'add_attribute' ), attribute => {
|
|
126
|
+
mergeAggregationsInWorkflowResult( aggregateAttributes( [ attribute ] ) );
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Invoke a step and unwraps the result to extract and merge "aggregations" and return only the output.
|
|
131
|
+
*
|
|
132
|
+
* @IMPORTANT Keep support for deprecated non-wrapped activity result to allow for Temporal replays.
|
|
133
|
+
* @TODO [OUT-468]
|
|
134
|
+
* @param {Function} step
|
|
135
|
+
* @param {...any} args
|
|
136
|
+
* @returns {any} The step "output"
|
|
137
|
+
*/
|
|
138
|
+
const callStepAndUnwrapResult = async ( step, ...args ) => {
|
|
139
|
+
const result = await step( ...args );
|
|
140
|
+
if ( !isActivityResultWrapped( result ) ) {
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
const { output, aggregations } = result;
|
|
144
|
+
if ( aggregations ) {
|
|
145
|
+
mergeAggregationsInWorkflowResult( aggregations );
|
|
146
|
+
}
|
|
147
|
+
return output;
|
|
148
|
+
};
|
|
86
149
|
|
|
87
150
|
try {
|
|
88
151
|
// validation comes after setting memo to have that info already set for interceptor even if validations fail
|
|
89
152
|
validateWithSchema( inputSchema, input, `Workflow ${name} input` );
|
|
90
153
|
|
|
91
154
|
const dispatchers = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* @param {boolean} extra.detached
|
|
104
|
-
* @param {import('@temporalio/workflow').ActivityOptions} extra.options
|
|
105
|
-
* @returns {Promise<unknown>}
|
|
106
|
-
*/
|
|
155
|
+
/* This are shortcuts to invoke activities as steps/evaluators both shared and non shared */
|
|
156
|
+
invokeStep: async ( stepName, input, options ) =>
|
|
157
|
+
callStepAndUnwrapResult( steps[`${name}#${stepName}`], input, options ),
|
|
158
|
+
invokeSharedStep: async ( stepName, input, options ) =>
|
|
159
|
+
callStepAndUnwrapResult( steps[`${SHARED_STEP_PREFIX}#${stepName}`], input, options ),
|
|
160
|
+
invokeEvaluator: async ( evaluatorName, input, options ) =>
|
|
161
|
+
callStepAndUnwrapResult( steps[`${name}#${evaluatorName}`], input, options ),
|
|
162
|
+
invokeSharedEvaluator: async ( evaluatorName, input, options ) =>
|
|
163
|
+
callStepAndUnwrapResult( steps[`${SHARED_STEP_PREFIX}#${evaluatorName}`], input, options ),
|
|
164
|
+
|
|
165
|
+
// Start a new child workflow
|
|
107
166
|
startWorkflow: async ( childName, input, extra = {} ) => {
|
|
108
167
|
try {
|
|
109
168
|
const result = await executeChild( childName, {
|
|
@@ -116,33 +175,43 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
116
175
|
...( extra?.options?.activityOptions && { activityOptions: deepMerge( activityOptions, extra.options.activityOptions ) } )
|
|
117
176
|
}
|
|
118
177
|
} );
|
|
119
|
-
|
|
178
|
+
/**
|
|
179
|
+
* @IMPORTANT Keep support for deprecated ".attributes" from workflow results to allow for Temporal replays.
|
|
180
|
+
* @TODO [OUT-468]
|
|
181
|
+
*/
|
|
182
|
+
if ( result?.attributes ) {
|
|
183
|
+
mergeAggregationsInWorkflowResult( aggregateAttributes( result.attributes ) );
|
|
184
|
+
}
|
|
185
|
+
if ( result?.aggregations ) {
|
|
186
|
+
mergeAggregationsInWorkflowResult( result.aggregations );
|
|
187
|
+
}
|
|
120
188
|
return result.output;
|
|
121
189
|
} catch ( error ) {
|
|
122
|
-
|
|
190
|
+
/**
|
|
191
|
+
* @IMPORTANT Keep support for deprecated ".attributes" from workflow errors to allow for Temporal replays.
|
|
192
|
+
* @TODO [OUT-468]
|
|
193
|
+
*/
|
|
194
|
+
const attributesFromError = extractErrorDetail( error, 'attributes' );
|
|
195
|
+
if ( attributesFromError ) {
|
|
196
|
+
mergeAggregationsInWorkflowResult( aggregateAttributes( attributesFromError ) );
|
|
197
|
+
}
|
|
198
|
+
const aggregationsFromError = extractErrorDetail( error, 'aggregations' );
|
|
199
|
+
if ( aggregationsFromError ) {
|
|
200
|
+
mergeAggregationsInWorkflowResult( aggregationsFromError );
|
|
201
|
+
}
|
|
123
202
|
throw error;
|
|
124
203
|
}
|
|
125
204
|
}
|
|
126
205
|
};
|
|
127
206
|
|
|
128
|
-
|
|
207
|
+
workflowResult.output = await fn.call( dispatchers, input, context );
|
|
129
208
|
|
|
130
|
-
validateWithSchema( outputSchema, output, `Workflow ${name} output` );
|
|
131
|
-
|
|
132
|
-
if ( isRoot ) {
|
|
133
|
-
// Append the trace info to the result of the workflow
|
|
134
|
-
return { output, trace: { destinations: traceDestinations }, attributes, aggregations: aggregateAttributes( attributes ) };
|
|
135
|
-
}
|
|
209
|
+
validateWithSchema( outputSchema, workflowResult.output, `Workflow ${name} output` );
|
|
136
210
|
|
|
137
|
-
return
|
|
211
|
+
return workflowResult;
|
|
138
212
|
} catch ( e ) {
|
|
139
|
-
// Append the
|
|
140
|
-
e[METADATA_ACCESS_SYMBOL] = { ...( e[METADATA_ACCESS_SYMBOL] ?? {} ),
|
|
141
|
-
// if it is roo also add trace/aggregations
|
|
142
|
-
if ( isRoot ) {
|
|
143
|
-
e[METADATA_ACCESS_SYMBOL].trace = { destinations: traceDestinations };
|
|
144
|
-
e[METADATA_ACCESS_SYMBOL].aggregations = aggregateAttributes( attributes );
|
|
145
|
-
}
|
|
213
|
+
// Append the result as metadata of the error, so it can be read by the interceptor.
|
|
214
|
+
e[METADATA_ACCESS_SYMBOL] = { ...( e[METADATA_ACCESS_SYMBOL] ?? {} ), ...workflowResult };
|
|
146
215
|
throw e;
|
|
147
216
|
}
|
|
148
217
|
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { ACTIVITY_GET_TRACE_DESTINATIONS, METADATA_ACCESS_SYMBOL, WORKFLOW_WRAPPER_VERSION_FIELD } from '#consts';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const inWorkflowContextMock = vi.hoisted( () => vi.fn( () => true ) );
|
|
6
|
+
const defineSignalMock = vi.hoisted( () => vi.fn( name => name ) );
|
|
7
|
+
const setHandlerMock = vi.hoisted( () => vi.fn() );
|
|
8
|
+
const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
|
|
9
|
+
const executeChildMock = vi.fn().mockResolvedValue( undefined );
|
|
10
|
+
const continueAsNewMock = vi.fn().mockResolvedValue( undefined );
|
|
11
|
+
|
|
12
|
+
const createStepsProxy = ( stepSpy = vi.fn() ) =>
|
|
13
|
+
new Proxy( {}, {
|
|
14
|
+
get: ( _, prop ) => {
|
|
15
|
+
if ( prop === ACTIVITY_GET_TRACE_DESTINATIONS ) {
|
|
16
|
+
return traceDestinationsStepMock;
|
|
17
|
+
}
|
|
18
|
+
if ( typeof prop === 'string' && prop.includes( '#' ) ) {
|
|
19
|
+
return stepSpy;
|
|
20
|
+
}
|
|
21
|
+
return vi.fn();
|
|
22
|
+
}
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
const proxyActivitiesMock = vi.fn( () => createStepsProxy() );
|
|
26
|
+
|
|
27
|
+
const workflowInfoReturn = {
|
|
28
|
+
workflowId: 'wf-test-123',
|
|
29
|
+
workflowType: 'test_wf',
|
|
30
|
+
memo: {},
|
|
31
|
+
startTime: new Date( '2025-01-01T00:00:00Z' ),
|
|
32
|
+
continueAsNewSuggested: false
|
|
33
|
+
};
|
|
34
|
+
const workflowInfoMock = vi.fn( () => ( { ...workflowInfoReturn } ) );
|
|
35
|
+
|
|
36
|
+
vi.mock( '@temporalio/workflow', () => ( {
|
|
37
|
+
proxyActivities: ( ...args ) => proxyActivitiesMock( ...args ),
|
|
38
|
+
inWorkflowContext: inWorkflowContextMock,
|
|
39
|
+
executeChild: ( ...args ) => executeChildMock( ...args ),
|
|
40
|
+
workflowInfo: workflowInfoMock,
|
|
41
|
+
uuid4: () => '550e8400e29b41d4a716446655440000',
|
|
42
|
+
ParentClosePolicy: { TERMINATE: 'TERMINATE', ABANDON: 'ABANDON' },
|
|
43
|
+
ChildWorkflowFailure: class ChildWorkflowFailure extends Error {
|
|
44
|
+
constructor( message, cause ) {
|
|
45
|
+
super( message );
|
|
46
|
+
this.name = 'ChildWorkflowFailure';
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
continueAsNew: continueAsNewMock,
|
|
51
|
+
defineSignal: ( ...args ) => defineSignalMock( ...args ),
|
|
52
|
+
setHandler: ( ...args ) => setHandlerMock( ...args )
|
|
53
|
+
} ) );
|
|
54
|
+
|
|
55
|
+
vi.mock( '#consts', async importOriginal => {
|
|
56
|
+
const actual = await importOriginal();
|
|
57
|
+
return {
|
|
58
|
+
...actual,
|
|
59
|
+
SHARED_STEP_PREFIX: '__shared',
|
|
60
|
+
ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations'
|
|
61
|
+
};
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
describe( 'workflow() replay compatibility', () => {
|
|
65
|
+
beforeEach( () => {
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
inWorkflowContextMock.mockReturnValue( true );
|
|
68
|
+
defineSignalMock.mockImplementation( name => name );
|
|
69
|
+
workflowInfoReturn.memo = {};
|
|
70
|
+
workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
|
|
71
|
+
proxyActivitiesMock.mockImplementation( () => createStepsProxy() );
|
|
72
|
+
traceDestinationsStepMock.mockResolvedValue( { local: '/tmp/trace' } );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'preserves old plain trace destination activity results', async () => {
|
|
76
|
+
const { workflow } = await import( './workflow.js' );
|
|
77
|
+
|
|
78
|
+
const wf = workflow( {
|
|
79
|
+
name: 'root_wf',
|
|
80
|
+
description: 'Root',
|
|
81
|
+
inputSchema: z.object( {} ),
|
|
82
|
+
outputSchema: z.object( { v: z.number() } ),
|
|
83
|
+
fn: async () => ( { v: 42 } )
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
const result = await wf( {} );
|
|
87
|
+
expect( result ).toEqual( {
|
|
88
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
89
|
+
output: { v: 42 },
|
|
90
|
+
trace: { destinations: { local: '/tmp/trace' } },
|
|
91
|
+
aggregations: null
|
|
92
|
+
} );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'converts old add_attribute signals into aggregations', async () => {
|
|
96
|
+
const { workflow } = await import( './workflow.js' );
|
|
97
|
+
const { Attribute } = await import( '#trace_attribute' );
|
|
98
|
+
const handlers = { addAttribute: () => {} };
|
|
99
|
+
setHandlerMock.mockImplementation( ( signalName, handler ) => {
|
|
100
|
+
if ( signalName === 'add_attribute' ) {
|
|
101
|
+
handlers.addAttribute = handler;
|
|
102
|
+
}
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
const httpRequest = {
|
|
106
|
+
type: Attribute.HTTPRequestCount.TYPE,
|
|
107
|
+
url: 'https://api.example.test/items',
|
|
108
|
+
requestId: 'req-1'
|
|
109
|
+
};
|
|
110
|
+
const httpCost = {
|
|
111
|
+
type: Attribute.HTTPRequestCost.TYPE,
|
|
112
|
+
url: 'https://api.example.test/items',
|
|
113
|
+
requestId: 'req-1',
|
|
114
|
+
total: 2.5
|
|
115
|
+
};
|
|
116
|
+
const llmUsage = {
|
|
117
|
+
type: Attribute.LLMUsage.TYPE,
|
|
118
|
+
modelId: 'gpt-4o',
|
|
119
|
+
total: 0.25,
|
|
120
|
+
usage: [
|
|
121
|
+
{ type: 'input', ppm: 5, amount: 20_000, total: 0.1 },
|
|
122
|
+
{ type: 'output', ppm: 30, amount: 5_000, total: 0.15 }
|
|
123
|
+
],
|
|
124
|
+
tokensUsed: 25_000
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const wf = workflow( {
|
|
128
|
+
name: 'attr_wf',
|
|
129
|
+
description: 'Attributes',
|
|
130
|
+
inputSchema: z.object( {} ),
|
|
131
|
+
outputSchema: z.object( { ok: z.boolean() } ),
|
|
132
|
+
fn: async () => {
|
|
133
|
+
handlers.addAttribute( httpRequest );
|
|
134
|
+
handlers.addAttribute( httpCost );
|
|
135
|
+
handlers.addAttribute( llmUsage );
|
|
136
|
+
return { ok: true };
|
|
137
|
+
}
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
const result = await wf( {} );
|
|
141
|
+
expect( result ).not.toHaveProperty( 'attributes' );
|
|
142
|
+
expect( result.aggregations ).toEqual( {
|
|
143
|
+
cost: { total: 2.75 },
|
|
144
|
+
tokens: {
|
|
145
|
+
total: 25_000,
|
|
146
|
+
input: 20_000,
|
|
147
|
+
output: 5_000
|
|
148
|
+
},
|
|
149
|
+
httpRequests: { total: 1 }
|
|
150
|
+
} );
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
it( 'preserves old plain activity results from steps', async () => {
|
|
154
|
+
const stepSpy = vi.fn().mockResolvedValue( { legacy: true } );
|
|
155
|
+
proxyActivitiesMock.mockImplementation( () => createStepsProxy( stepSpy ) );
|
|
156
|
+
const { workflow } = await import( './workflow.js' );
|
|
157
|
+
|
|
158
|
+
const wf = workflow( {
|
|
159
|
+
name: 'legacy_step_wf',
|
|
160
|
+
description: 'Legacy step result',
|
|
161
|
+
inputSchema: z.object( {} ),
|
|
162
|
+
outputSchema: z.object( { legacy: z.boolean() } ),
|
|
163
|
+
async fn() {
|
|
164
|
+
return this.invokeStep( 'myStep', { foo: 1 } );
|
|
165
|
+
}
|
|
166
|
+
} );
|
|
167
|
+
|
|
168
|
+
const result = await wf( {} );
|
|
169
|
+
expect( result.output ).toEqual( { legacy: true } );
|
|
170
|
+
expect( result.aggregations ).toEqual( null );
|
|
171
|
+
} );
|
|
172
|
+
|
|
173
|
+
it( 'converts old child workflow attributes into parent aggregations', async () => {
|
|
174
|
+
const { workflow } = await import( './workflow.js' );
|
|
175
|
+
const { Attribute } = await import( '#trace_attribute' );
|
|
176
|
+
const childAttribute = {
|
|
177
|
+
type: Attribute.LLMUsage.TYPE,
|
|
178
|
+
modelId: 'gpt-4o',
|
|
179
|
+
total: 0.4,
|
|
180
|
+
tokensUsed: 20,
|
|
181
|
+
usage: [
|
|
182
|
+
{ type: 'input', ppm: 10, amount: 20, total: 0.4 }
|
|
183
|
+
]
|
|
184
|
+
};
|
|
185
|
+
executeChildMock.mockResolvedValueOnce( {
|
|
186
|
+
output: { child: 'ok' },
|
|
187
|
+
attributes: [ childAttribute ]
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
const wf = workflow( {
|
|
191
|
+
name: 'merge_child_wf',
|
|
192
|
+
description: 'Merge child attributes',
|
|
193
|
+
inputSchema: z.object( {} ),
|
|
194
|
+
outputSchema: z.object( { child: z.string() } ),
|
|
195
|
+
async fn() {
|
|
196
|
+
return this.startWorkflow( 'child_wf', { id: 1 } );
|
|
197
|
+
}
|
|
198
|
+
} );
|
|
199
|
+
|
|
200
|
+
const result = await wf( {} );
|
|
201
|
+
expect( result ).toEqual( {
|
|
202
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
203
|
+
output: { child: 'ok' },
|
|
204
|
+
trace: { destinations: { local: '/tmp/trace' } },
|
|
205
|
+
aggregations: {
|
|
206
|
+
cost: { total: 0.4 },
|
|
207
|
+
tokens: {
|
|
208
|
+
total: 20,
|
|
209
|
+
input: 20
|
|
210
|
+
},
|
|
211
|
+
httpRequests: { total: 0 }
|
|
212
|
+
}
|
|
213
|
+
} );
|
|
214
|
+
} );
|
|
215
|
+
|
|
216
|
+
it( 'converts old child workflow error attributes into parent error metadata aggregations', async () => {
|
|
217
|
+
const { workflow } = await import( './workflow.js' );
|
|
218
|
+
const { ChildWorkflowFailure } = await import( '@temporalio/workflow' );
|
|
219
|
+
const { Attribute } = await import( '#trace_attribute' );
|
|
220
|
+
const childAttribute = {
|
|
221
|
+
type: Attribute.HTTPRequestCost.TYPE,
|
|
222
|
+
url: 'https://api.example.test',
|
|
223
|
+
requestId: 'req-child',
|
|
224
|
+
total: 2
|
|
225
|
+
};
|
|
226
|
+
const childError = new ChildWorkflowFailure( 'child failed', {
|
|
227
|
+
message: 'Child workflow execution failed',
|
|
228
|
+
details: [ { attributes: [ childAttribute ] } ]
|
|
229
|
+
} );
|
|
230
|
+
executeChildMock.mockRejectedValueOnce( childError );
|
|
231
|
+
|
|
232
|
+
const wf = workflow( {
|
|
233
|
+
name: 'child_error_wf',
|
|
234
|
+
description: 'Child error attributes',
|
|
235
|
+
inputSchema: z.object( {} ),
|
|
236
|
+
outputSchema: z.object( {} ),
|
|
237
|
+
async fn() {
|
|
238
|
+
await this.startWorkflow( 'child_wf', { id: 1 } );
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
} );
|
|
242
|
+
|
|
243
|
+
await expect( wf( {} ) ).rejects.toThrow( 'child failed' );
|
|
244
|
+
expect( childError[METADATA_ACCESS_SYMBOL] ).toEqual( {
|
|
245
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
246
|
+
trace: { destinations: { local: '/tmp/trace' } },
|
|
247
|
+
aggregations: {
|
|
248
|
+
cost: { total: 2 },
|
|
249
|
+
tokens: { total: 0 },
|
|
250
|
+
httpRequests: { total: 0 }
|
|
251
|
+
}
|
|
252
|
+
} );
|
|
253
|
+
} );
|
|
254
|
+
} );
|