@outputai/core 0.8.1-next.e92f632.0 → 0.8.2-next.013689b.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 +8 -4
- package/src/interface/evaluator.js +1 -1
- package/src/interface/step.js +1 -1
- package/src/interface/workflow.js +2 -1
- package/src/internal_activities/index.js +2 -1
- package/src/internal_utils/component.js +9 -0
- package/src/sdk/README.md +47 -0
- package/src/sdk/helpers/component_metadata.d.ts +17 -0
- package/src/sdk/helpers/component_metadata.js +6 -0
- package/src/sdk/helpers/component_metadata.spec.js +30 -0
- package/src/sdk/helpers/index.d.ts +11 -0
- package/src/sdk/helpers/index.js +2 -0
- package/src/sdk/helpers/path.d.ts +11 -0
- package/src/sdk/helpers/path.js +32 -0
- package/src/{utils/resolve_invocation_dir.spec.js → sdk/helpers/path.spec.js} +9 -9
- package/src/sdk/runtime/context.d.ts +30 -0
- package/src/sdk/runtime/context.js +15 -0
- package/src/{activity_integration → sdk/runtime}/context.spec.js +5 -5
- package/src/sdk/runtime/events.d.ts +15 -0
- package/src/sdk/runtime/events.js +18 -0
- package/src/{activity_integration → sdk/runtime}/events.spec.js +8 -9
- package/src/sdk/runtime/index.d.ts +12 -0
- package/src/sdk/runtime/index.js +3 -0
- package/src/sdk/runtime/tracing.d.ts +46 -0
- package/src/sdk/runtime/tracing.js +11 -0
- package/src/utils/index.d.ts +0 -32
- package/src/utils/index.js +0 -1
- package/src/utils/utils.js +0 -27
- package/src/worker/configs.js +13 -0
- package/src/worker/configs.spec.js +34 -1
- package/src/worker/connection_monitor.js +2 -13
- package/src/worker/connection_monitor.spec.js +4 -21
- package/src/worker/index.js +7 -2
- package/src/worker/index.spec.js +22 -1
- package/src/worker/loader/tools.js +1 -1
- package/src/worker/loader/tools.spec.js +1 -1
- package/src/activity_integration/context.d.ts +0 -23
- package/src/activity_integration/context.js +0 -18
- package/src/activity_integration/event_id_integration.spec.js +0 -52
- package/src/activity_integration/events.d.ts +0 -10
- package/src/activity_integration/events.js +0 -15
- package/src/activity_integration/index.d.ts +0 -9
- package/src/activity_integration/index.js +0 -3
- package/src/activity_integration/tracing.d.ts +0 -40
- package/src/activity_integration/tracing.js +0 -48
- package/src/utils/resolve_invocation_dir.js +0 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2-next.013689b.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
"types": "./src/hooks/index.d.ts",
|
|
13
13
|
"import": "./src/hooks/index.js"
|
|
14
14
|
},
|
|
15
|
-
"./
|
|
16
|
-
"types": "./src/
|
|
17
|
-
"import": "./src/
|
|
15
|
+
"./sdk/helpers": {
|
|
16
|
+
"types": "./src/sdk/helpers/index.d.ts",
|
|
17
|
+
"import": "./src/sdk/helpers/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./sdk/runtime": {
|
|
20
|
+
"types": "./src/sdk/runtime/index.d.ts",
|
|
21
|
+
"import": "./src/sdk/runtime/index.js"
|
|
18
22
|
},
|
|
19
23
|
"./sdk_utils": {
|
|
20
24
|
"types": "./src/utils/index.d.ts",
|
package/src/interface/step.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy } from '@temporalio/workflow';
|
|
3
3
|
import { WorkflowValidator } from './validations/index.js';
|
|
4
|
-
import { deepMerge,
|
|
4
|
+
import { deepMerge, toUrlSafeBase64 } from '#utils';
|
|
5
5
|
import { WorkflowContext } from '#internal_utils/workflow_context';
|
|
6
|
+
import { setMetadata } from '#internal_utils/component';
|
|
6
7
|
import { TraceInfo } from '#internal_utils/trace_info';
|
|
7
8
|
import { defaultOptions } from './workflow_activity_options.js';
|
|
8
9
|
import {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FatalError } from '#errors';
|
|
2
2
|
import { fetch } from 'undici';
|
|
3
|
-
import {
|
|
3
|
+
import { serializeFetchResponse, serializeBodyAndInferContentType } from '#utils';
|
|
4
|
+
import { setMetadata } from '#internal_utils/component';
|
|
4
5
|
import { ComponentType } from '#consts';
|
|
5
6
|
import { createChildLogger } from '#logger';
|
|
6
7
|
import { getDestinations } from '#tracing';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add metadata "values" property to a given object
|
|
5
|
+
* @param {object} target
|
|
6
|
+
* @param {object} values
|
|
7
|
+
*/
|
|
8
|
+
export const setMetadata = ( target, values ) =>
|
|
9
|
+
Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## SDK integration API for Output packages
|
|
2
|
+
|
|
3
|
+
**THESE MODULES ARE NOT PUBLIC. DO NOT IMPORT THEM ON YOUR PROJECT.**
|
|
4
|
+
|
|
5
|
+
This folder contains integration helpers used by other Output SDK packages.
|
|
6
|
+
|
|
7
|
+
The two subfolders represent a hard Temporal boundary. Code imported by
|
|
8
|
+
workflow-bundled modules must only depend on `helpers`. Code that needs worker
|
|
9
|
+
or activity runtime state belongs in `runtime`.
|
|
10
|
+
|
|
11
|
+
Do not change these without reviewing call-sites.
|
|
12
|
+
|
|
13
|
+
### `helpers`
|
|
14
|
+
|
|
15
|
+
Helpers must be safe to import while Temporal workflows are bundled and executed
|
|
16
|
+
in the workflow sandbox.
|
|
17
|
+
|
|
18
|
+
Allowed:
|
|
19
|
+
|
|
20
|
+
- Stateless functions and namespace objects.
|
|
21
|
+
- Deterministic code with no runtime side effects at import time.
|
|
22
|
+
- Imports from other sandbox-safe helper modules.
|
|
23
|
+
- Small, dependency-free helpers for any deterministic computation that can run
|
|
24
|
+
without worker or activity state.
|
|
25
|
+
|
|
26
|
+
Not allowed:
|
|
27
|
+
|
|
28
|
+
- Access to activity context, async storage, sinks, tracing state, workers, or
|
|
29
|
+
clients.
|
|
30
|
+
- Imports from `@temporalio/activity`, `@temporalio/worker`, or Node-only APIs
|
|
31
|
+
that cannot be bundled into workflow code.
|
|
32
|
+
- Process, filesystem, network, clock, randomness, or environment access unless
|
|
33
|
+
the behavior is known to be safe and deterministic in the workflow sandbox.
|
|
34
|
+
- Any import chain that reaches `runtime`.
|
|
35
|
+
|
|
36
|
+
If a helper cannot satisfy these rules, put it in `runtime` instead.
|
|
37
|
+
|
|
38
|
+
### `runtime`
|
|
39
|
+
|
|
40
|
+
Runtime modules are for helpers that need Output or Temporal runtime state.
|
|
41
|
+
|
|
42
|
+
Use `runtime` for activity-only integrations such as context lookup, event
|
|
43
|
+
emission, tracing, async storage, worker state, sinks, and other behavior that is
|
|
44
|
+
not safe inside workflow-bundled code.
|
|
45
|
+
|
|
46
|
+
Runtime modules may depend on activity runtime APIs and internal state, but they
|
|
47
|
+
must not be imported by workflow-safe code.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools to extract data from Outputai Core Components metadata storage
|
|
3
|
+
*/
|
|
4
|
+
export declare const ComponentMetadata: {
|
|
5
|
+
/**
|
|
6
|
+
* Extract the name from a workflow(), step() or evaluator() return
|
|
7
|
+
* @param fn
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
getName( fn: Function ): string | undefined;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a function has metadata from workflow(), step() or evaluator()
|
|
14
|
+
* @param fn
|
|
15
|
+
*/
|
|
16
|
+
has( fn: Function ): boolean;
|
|
17
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
3
|
+
import { ComponentMetadata } from './component_metadata.js';
|
|
4
|
+
|
|
5
|
+
describe( 'ComponentMetadata', () => {
|
|
6
|
+
it( 'detects functions tagged with component metadata', () => {
|
|
7
|
+
const component = () => {};
|
|
8
|
+
Object.defineProperty( component, METADATA_ACCESS_SYMBOL, {
|
|
9
|
+
value: { name: 'my_component' }
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
expect( ComponentMetadata.has( component ) ).toBe( true );
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
it( 'returns the component metadata name', () => {
|
|
16
|
+
const component = () => {};
|
|
17
|
+
Object.defineProperty( component, METADATA_ACCESS_SYMBOL, {
|
|
18
|
+
value: { name: 'my_component' }
|
|
19
|
+
} );
|
|
20
|
+
|
|
21
|
+
expect( ComponentMetadata.getName( component ) ).toBe( 'my_component' );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'returns false and undefined for untagged functions', () => {
|
|
25
|
+
const fn = () => {};
|
|
26
|
+
|
|
27
|
+
expect( ComponentMetadata.has( fn ) ).toBe( false );
|
|
28
|
+
expect( ComponentMetadata.getName( fn ) ).toBeUndefined();
|
|
29
|
+
} );
|
|
30
|
+
} );
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* > [!WARNING]
|
|
3
|
+
* > **Internal use only.** Not part of the public API; may change without notice.
|
|
4
|
+
*
|
|
5
|
+
* These are stateless helpers for other SDK modules integration.
|
|
6
|
+
* They can be used while Temporal workflows are bundled.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export * from './component_metadata.js';
|
|
11
|
+
export * from './path.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools to interact with filesystem paths
|
|
3
|
+
*/
|
|
4
|
+
export declare const Path: {
|
|
5
|
+
/**
|
|
6
|
+
* Return the first immediate directory of the file invoking the code that called this function.
|
|
7
|
+
*
|
|
8
|
+
* Excludes `@outputai/core`, node, other internal paths, and any additional ignore paths.
|
|
9
|
+
*/
|
|
10
|
+
resolveInvocationDir( additionalIgnorePaths?: string[] ): string
|
|
11
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as stackTraceParser from 'stacktrace-parser';
|
|
2
|
+
|
|
3
|
+
// OS separator, but in a deterministic way, allowing this to work in Temporal's sandbox
|
|
4
|
+
// This avoids importing from node:path
|
|
5
|
+
const SEP = new Error().stack.includes( '/' ) ? '/' : '\\';
|
|
6
|
+
const transformSeparators = path => path.replaceAll( '/', SEP );
|
|
7
|
+
|
|
8
|
+
const defaultIgnorePaths = [
|
|
9
|
+
'/@outputai/core/',
|
|
10
|
+
'/@outputai/llm/',
|
|
11
|
+
'/@outputai/evals/',
|
|
12
|
+
'/sdk/core/',
|
|
13
|
+
'/sdk/llm/',
|
|
14
|
+
'/sdk/evals/',
|
|
15
|
+
'node:internal/',
|
|
16
|
+
'evalmachine.',
|
|
17
|
+
'webpack/bootstrap'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const Path = {
|
|
21
|
+
resolveInvocationDir: ( additionalIgnorePaths = [] ) => {
|
|
22
|
+
const stack = new Error().stack;
|
|
23
|
+
const lines = stackTraceParser.parse( stack );
|
|
24
|
+
const ignorePaths = [ ...additionalIgnorePaths, ...defaultIgnorePaths ].map( transformSeparators );
|
|
25
|
+
|
|
26
|
+
const frame = lines.find( l => !ignorePaths.some( p => l.file.includes( p ) ) );
|
|
27
|
+
if ( !frame ) {
|
|
28
|
+
throw new Error( `Invocation dir resolution via stack trace failed. Stack: ${stack}` );
|
|
29
|
+
}
|
|
30
|
+
return frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import
|
|
2
|
+
import { Path } from './path.js';
|
|
3
3
|
|
|
4
4
|
const OriginalError = Error;
|
|
5
5
|
|
|
@@ -10,7 +10,7 @@ describe( 'Resolve Invocation Dir', () => {
|
|
|
10
10
|
|
|
11
11
|
it( 'Should detect the invocation dir from the tests workflow', () => {
|
|
12
12
|
const stack = `Error
|
|
13
|
-
at resolveInvocationDir (file:///app/sdk/core/src/
|
|
13
|
+
at resolveInvocationDir (file:///app/sdk/core/src/sdk/helpers/path.js)
|
|
14
14
|
at fn (file:///app/test_workflows/dist/simple/steps.js:8:21)
|
|
15
15
|
at wrapper (file:///app/sdk/core/src/interface/step.js:12:26)
|
|
16
16
|
at executeNextHandler (/app/node_modules/@temporalio/worker/lib/activity.js:99:54)
|
|
@@ -29,12 +29,12 @@ describe( 'Resolve Invocation Dir', () => {
|
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/simple' );
|
|
32
|
+
expect( Path.resolveInvocationDir() ).toBe( '/app/test_workflows/dist/simple' );
|
|
33
33
|
} );
|
|
34
34
|
|
|
35
35
|
it( 'Should detect the invocation dir from the sandbox environment at sdk/core', () => {
|
|
36
36
|
const stack = `Error
|
|
37
|
-
at resolveInvocationDir (file:///app/sdk/core/src/
|
|
37
|
+
at resolveInvocationDir (file:///app/sdk/core/src/sdk/helpers/path.js)
|
|
38
38
|
at workflow (file:///app/sdk/core/src/interface/workflow.js:25:16)
|
|
39
39
|
at file:///app/test_workflows/dist/nested/workflow.js:4:16
|
|
40
40
|
at ModuleJob.run (node:internal/modules/esm/module_job:365:25)
|
|
@@ -50,12 +50,12 @@ describe( 'Resolve Invocation Dir', () => {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
|
|
53
|
+
expect( Path.resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
|
|
54
54
|
} );
|
|
55
55
|
|
|
56
56
|
it( 'Should detect the invocation dir from workflow loading at core', () => {
|
|
57
57
|
const stack = `Error
|
|
58
|
-
at
|
|
58
|
+
at resolveInvocationDir (/app/sdk/core/src/sdk/helpers/path.js:21:0)
|
|
59
59
|
at workflow (/app/sdk/core/src/interface/workflow.js:22:43)
|
|
60
60
|
at ../../test_workflows/dist/nested/workflow.js (/app/test_workflows/dist/nested/workflow.js:4:24)
|
|
61
61
|
at __webpack_require__ (webpack/bootstrap:19:0)
|
|
@@ -73,12 +73,12 @@ describe( 'Resolve Invocation Dir', () => {
|
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
expect( resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
|
|
76
|
+
expect( Path.resolveInvocationDir() ).toBe( '/app/test_workflows/dist/nested' );
|
|
77
77
|
} );
|
|
78
78
|
|
|
79
79
|
it( 'Should detect the invocation dir from a workflow using core installed via NPM', () => {
|
|
80
80
|
const stack = `Error
|
|
81
|
-
at resolveInvocationDir (file:///app/node_modules/@outputai/core/src/
|
|
81
|
+
at resolveInvocationDir (file:///app/node_modules/@outputai/core/src/sdk/helpers/path.js)
|
|
82
82
|
at fn (file:///app/dist/simple/steps.js:8:21)
|
|
83
83
|
at wrapper (file:///app/node_modules/@outputai/core/src/interface/step.js:12:26)
|
|
84
84
|
at executeNextHandler (/app/node_modules/@temporalio/worker/lib/activity.js:99:54)
|
|
@@ -97,6 +97,6 @@ describe( 'Resolve Invocation Dir', () => {
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
expect( resolveInvocationDir() ).toBe( '/app/dist/simple' );
|
|
100
|
+
expect( Path.resolveInvocationDir() ).toBe( '/app/dist/simple' );
|
|
101
101
|
} );
|
|
102
102
|
} );
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Info } from '@temporalio/activity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context object
|
|
5
|
+
*/
|
|
6
|
+
export type Context = {
|
|
7
|
+
/** Temporal info about the current activity */
|
|
8
|
+
activityInfo: Info,
|
|
9
|
+
/** Path of the workflow file */
|
|
10
|
+
workflowFilename: string
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tools to interact with Runtime context
|
|
15
|
+
*/
|
|
16
|
+
export declare const Context: {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns information about the current Temporal execution.
|
|
20
|
+
*
|
|
21
|
+
* Only available when called from within a step or evaluator (Temporal Activities) running in the Temporal runtime.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* - Returns `null` when not called inside a Temporal Activity (steps/evaluators);
|
|
25
|
+
* - Returns `null` when not called from within a running Temporal worker, like in unit tests environment;
|
|
26
|
+
*
|
|
27
|
+
* @returns The workflow context, or `null` if unavailable or incomplete.
|
|
28
|
+
*/
|
|
29
|
+
getActivityContext(): Context | null;
|
|
30
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Storage } from '#async_storage';
|
|
2
|
+
|
|
3
|
+
export const Context = {
|
|
4
|
+
getActivityContext: () => {
|
|
5
|
+
const ctx = Storage.load();
|
|
6
|
+
if ( !ctx ) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
workflowFilename: ctx.workflowFilename,
|
|
12
|
+
activityInfo: ctx.activityInfo
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -5,7 +5,7 @@ vi.mock( '#async_storage', () => ( {
|
|
|
5
5
|
Storage: { load: loadMock }
|
|
6
6
|
} ) );
|
|
7
7
|
|
|
8
|
-
describe( '
|
|
8
|
+
describe( 'Context.getActivityContext', () => {
|
|
9
9
|
beforeEach( () => {
|
|
10
10
|
vi.clearAllMocks();
|
|
11
11
|
vi.resetModules();
|
|
@@ -13,8 +13,8 @@ describe( 'getExecutionContext', () => {
|
|
|
13
13
|
|
|
14
14
|
it( 'returns null when no context is stored', async () => {
|
|
15
15
|
loadMock.mockReturnValue( undefined );
|
|
16
|
-
const {
|
|
17
|
-
expect(
|
|
16
|
+
const { Context } = await import( './index.js' );
|
|
17
|
+
expect( Context.getActivityContext() ).toBeNull();
|
|
18
18
|
} );
|
|
19
19
|
|
|
20
20
|
it( 'returns activity execution context from storage', async () => {
|
|
@@ -28,8 +28,8 @@ describe( 'getExecutionContext', () => {
|
|
|
28
28
|
activityInfo,
|
|
29
29
|
workflowFilename: '/workflows/myWorkflow.js'
|
|
30
30
|
} );
|
|
31
|
-
const {
|
|
32
|
-
expect(
|
|
31
|
+
const { Context } = await import( './index.js' );
|
|
32
|
+
expect( Context.getActivityContext() ).toEqual( {
|
|
33
33
|
activityInfo,
|
|
34
34
|
workflowFilename: '/workflows/myWorkflow.js'
|
|
35
35
|
} );
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools to interact with Events
|
|
3
|
+
*/
|
|
4
|
+
export declare const Event: {
|
|
5
|
+
/**
|
|
6
|
+
* Emits a custom event on the in-process message bus.
|
|
7
|
+
*
|
|
8
|
+
* When called inside an Output activity context, the framework automatically
|
|
9
|
+
* attaches `activityInfo`, `workflowDetails`, and `outputActivityKind` onto the emitted payload.
|
|
10
|
+
*
|
|
11
|
+
* @param eventName - The name of the event to emit
|
|
12
|
+
* @param payload - An optional payload to send to the event
|
|
13
|
+
*/
|
|
14
|
+
emit( eventName: string, payload?: object ): void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { messageBus } from '#bus';
|
|
2
|
+
import { Storage } from '#async_storage';
|
|
3
|
+
|
|
4
|
+
export const Event = {
|
|
5
|
+
emit: ( eventName, payload ) => {
|
|
6
|
+
const ctx = Storage.load();
|
|
7
|
+
|
|
8
|
+
messageBus.emit( `external:${eventName}`, {
|
|
9
|
+
...payload ?? {},
|
|
10
|
+
...( ctx && {
|
|
11
|
+
activityInfo: ctx.activityInfo,
|
|
12
|
+
workflowDetails: ctx.workflowDetails,
|
|
13
|
+
outputActivityKind: ctx.outputActivityKind
|
|
14
|
+
} )
|
|
15
|
+
} );
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
@@ -11,12 +11,11 @@ vi.mock( '#bus', () => ( {
|
|
|
11
11
|
messageBus: { emit: emitMock }
|
|
12
12
|
} ) );
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { Event } from './events.js';
|
|
15
15
|
|
|
16
|
-
// `eventId` stamping is the bus layer's responsibility (see bus.spec.js
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
describe( 'emitEvent', () => {
|
|
16
|
+
// `eventId` stamping is the bus layer's responsibility (see bus.spec.js).
|
|
17
|
+
// Assertions here use `objectContaining` so they don't have to know about that enrichment.
|
|
18
|
+
describe( 'Event.emit', () => {
|
|
20
19
|
beforeEach( () => {
|
|
21
20
|
vi.clearAllMocks();
|
|
22
21
|
} );
|
|
@@ -39,7 +38,7 @@ describe( 'emitEvent', () => {
|
|
|
39
38
|
outputActivityKind: 'step'
|
|
40
39
|
} );
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
Event.emit( 'cost:llm:request', { modelId: 'gpt-4o' } );
|
|
43
42
|
|
|
44
43
|
expect( emitMock ).toHaveBeenCalledWith( 'external:cost:llm:request', expect.objectContaining( {
|
|
45
44
|
activityInfo,
|
|
@@ -52,7 +51,7 @@ describe( 'emitEvent', () => {
|
|
|
52
51
|
it( 'emits payload without context when storage is missing', () => {
|
|
53
52
|
loadMock.mockReturnValue( undefined );
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
Event.emit( 'foo:bar', { x: 1 } );
|
|
56
55
|
|
|
57
56
|
expect( emitMock ).toHaveBeenCalledWith( 'external:foo:bar', { x: 1 } );
|
|
58
57
|
} );
|
|
@@ -75,7 +74,7 @@ describe( 'emitEvent', () => {
|
|
|
75
74
|
outputActivityKind: 'step'
|
|
76
75
|
} );
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
Event.emit( 'lifecycle:start' );
|
|
79
78
|
|
|
80
79
|
expect( emitMock ).toHaveBeenCalledWith( 'external:lifecycle:start', expect.objectContaining( {
|
|
81
80
|
activityInfo,
|
|
@@ -102,7 +101,7 @@ describe( 'emitEvent', () => {
|
|
|
102
101
|
outputActivityKind: 'step'
|
|
103
102
|
} );
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
Event.emit( 'cost:http:request', {
|
|
106
105
|
activityInfo: { activityId: 'should-be-overridden' },
|
|
107
106
|
workflowDetails: { workflowId: 'should-be-overridden' },
|
|
108
107
|
outputActivityKind: 'should-be-overridden',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* > [!WARNING]
|
|
3
|
+
* > **Internal use only.** Not part of the public API; may change without notice.
|
|
4
|
+
*
|
|
5
|
+
* These are helpers for other SDK modules integration.
|
|
6
|
+
* These need Temporal activity runtime to work, as they access state, emit events and use node:* modules.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export * from './context.js';
|
|
11
|
+
export * from './events.js';
|
|
12
|
+
export * from './tracing.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Attribute } from '#trace_attribute';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tools to interact with Tracing
|
|
5
|
+
*/
|
|
6
|
+
export declare const Tracing: {
|
|
7
|
+
Attribute: typeof Attribute;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new event.
|
|
11
|
+
*
|
|
12
|
+
* @param args
|
|
13
|
+
* @param args.id - A unique id for the Event.
|
|
14
|
+
* @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
|
|
15
|
+
* @param args.name - The human-friendly name of the Event: query, request, create.
|
|
16
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
17
|
+
*/
|
|
18
|
+
addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Concludes an event.
|
|
22
|
+
*
|
|
23
|
+
* @param args
|
|
24
|
+
* @param args.id - The id of the event to conclude.
|
|
25
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
|
|
26
|
+
*/
|
|
27
|
+
addEventEnd( args: { id: string; details: unknown } ): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Concludes an event with an error.
|
|
31
|
+
*
|
|
32
|
+
* @param args
|
|
33
|
+
* @param args.id - The id of the event to conclude.
|
|
34
|
+
* @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
|
|
35
|
+
*/
|
|
36
|
+
addEventError( args: { id: string; details: unknown } ): void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adds an attribute to an event.
|
|
40
|
+
*
|
|
41
|
+
* @param args
|
|
42
|
+
* @param args.eventId - The id of the event to attach the attribute to.
|
|
43
|
+
* @param args.attribute - The attribute to attach to the event.
|
|
44
|
+
*/
|
|
45
|
+
addEventAttribute( args: { eventId: string; attribute: Attribute.Instance } ): void;
|
|
46
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { addEventActionWithContext as send, EventAction } from '#tracing';
|
|
2
|
+
|
|
3
|
+
import { Attribute } from '#trace_attribute';
|
|
4
|
+
|
|
5
|
+
export const Tracing = {
|
|
6
|
+
Attribute,
|
|
7
|
+
addEventStart: ( { id, kind, name, details } ) => send( EventAction.START, { kind, name, details, id } ),
|
|
8
|
+
addEventEnd: ( { id, details } ) => send( EventAction.END, { id, details } ),
|
|
9
|
+
addEventError: ( { id, details } ) => send( EventAction.ERROR, { id, details } ),
|
|
10
|
+
addEventAttribute: ( { eventId, attribute } ) => send( EventAction.ADD_ATTR, { id: eventId, details: attribute } )
|
|
11
|
+
};
|
package/src/utils/index.d.ts
CHANGED
|
@@ -5,13 +5,6 @@
|
|
|
5
5
|
* @packageDocumentation
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Return the first immediate directory of the file invoking the code that called this function.
|
|
10
|
-
*
|
|
11
|
-
* Excludes `@outputai/core`, node, and other internal paths.
|
|
12
|
-
*/
|
|
13
|
-
export function resolveInvocationDir(): string;
|
|
14
|
-
|
|
15
8
|
/**
|
|
16
9
|
* Node safe clone implementation that doesn't use global structuredClone().
|
|
17
10
|
*
|
|
@@ -23,31 +16,6 @@ export function resolveInvocationDir(): string;
|
|
|
23
16
|
*/
|
|
24
17
|
export function clone( object: object ): object;
|
|
25
18
|
|
|
26
|
-
/**
|
|
27
|
-
* Receives an error as argument and throws it.
|
|
28
|
-
*
|
|
29
|
-
* @param error
|
|
30
|
-
* @throws {Error}
|
|
31
|
-
*/
|
|
32
|
-
export function throws( error: Error ): void;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Attach given value to an object with the METADATA_ACCESS_SYMBOL symbol as key.
|
|
36
|
-
*
|
|
37
|
-
* @param target
|
|
38
|
-
* @param value
|
|
39
|
-
* @returns
|
|
40
|
-
*/
|
|
41
|
-
export function setMetadata( target: object, value: object ): void;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Read metadata previously attached via setMetadata.
|
|
45
|
-
*
|
|
46
|
-
* @param target - The function or object to read metadata from.
|
|
47
|
-
* @returns The metadata object, or null if none is attached.
|
|
48
|
-
*/
|
|
49
|
-
export function getMetadata( target: Function ): { name: string; description?: string; type?: string } | null;
|
|
50
|
-
|
|
51
19
|
/** Represents a {Response} serialized to plain object */
|
|
52
20
|
export type SerializedFetchResponse = {
|
|
53
21
|
/** The response url */
|
package/src/utils/index.js
CHANGED
package/src/utils/utils.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Node safe clone implementation that doesn't use global structuredClone()
|
|
5
3
|
* @param {object} v
|
|
@@ -25,31 +23,6 @@ export const isPlainObject = v =>
|
|
|
25
23
|
v !== null &&
|
|
26
24
|
[ Object.prototype, null ].includes( Object.getPrototypeOf( v ) );
|
|
27
25
|
|
|
28
|
-
/**
|
|
29
|
-
* Throw given error
|
|
30
|
-
* @param {Error} e
|
|
31
|
-
* @throws {e}
|
|
32
|
-
*/
|
|
33
|
-
export const throws = e => {
|
|
34
|
-
throw e;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Add metadata "values" property to a given object
|
|
39
|
-
* @param {object} target
|
|
40
|
-
* @param {object} values
|
|
41
|
-
* @returns
|
|
42
|
-
*/
|
|
43
|
-
export const setMetadata = ( target, values ) =>
|
|
44
|
-
Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Read metadata previously attached via setMetadata
|
|
48
|
-
* @param {Function} target
|
|
49
|
-
* @returns {object|null}
|
|
50
|
-
*/
|
|
51
|
-
export const getMetadata = target => target[METADATA_ACCESS_SYMBOL] ?? null;
|
|
52
|
-
|
|
53
26
|
/**
|
|
54
27
|
* Returns true if string value is stringbool and true
|
|
55
28
|
* @param {string} v
|
package/src/worker/configs.js
CHANGED
|
@@ -5,6 +5,13 @@ class InvalidEnvVarsErrors extends Error { }
|
|
|
5
5
|
|
|
6
6
|
const coalesceEmptyString = v => v === '' ? undefined : v;
|
|
7
7
|
|
|
8
|
+
const durationSchema = z.preprocess(
|
|
9
|
+
coalesceEmptyString,
|
|
10
|
+
z.string()
|
|
11
|
+
.regex( /^\d+$|^\d+(\.\d+)?\s?(ms|s|m|h|d)$/i )
|
|
12
|
+
.optional()
|
|
13
|
+
);
|
|
14
|
+
|
|
8
15
|
const envVarSchema = z.object( {
|
|
9
16
|
OUTPUT_CATALOG_ID: z.string().regex( /^[a-z0-9_.@-]+$/i ),
|
|
10
17
|
OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().nonnegative().default( 0 ) ),
|
|
@@ -28,6 +35,10 @@ const envVarSchema = z.object( {
|
|
|
28
35
|
OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: z.transform( v => v === undefined ? true : isStringboolTrue( v ) ),
|
|
29
36
|
// Time to allow for hooks to flush before shutdown
|
|
30
37
|
OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) ),
|
|
38
|
+
// Set temporal worker shutdown force time
|
|
39
|
+
TEMPORAL_SHUTDOWN_FORCE_TIME: durationSchema,
|
|
40
|
+
// Set temporal worker shutdown grace time
|
|
41
|
+
TEMPORAL_SHUTDOWN_GRACE_TIME: durationSchema,
|
|
31
42
|
// HTTP CONNECT proxy for Temporal gRPC connections (e.g. "proxy-host:8080").
|
|
32
43
|
// Must be a bare host:port — no scheme (Temporal's native HTTP CONNECT
|
|
33
44
|
// option is not a URL).
|
|
@@ -56,4 +67,6 @@ export const workerTelemetryIntervalMs = envVars.OUTPUT_WORKER_TELEMETRY_INTERVA
|
|
|
56
67
|
export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
|
|
57
68
|
export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
|
|
58
69
|
export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
|
|
70
|
+
export const shutdownForceTime = envVars.TEMPORAL_SHUTDOWN_FORCE_TIME;
|
|
71
|
+
export const shutdownGraceTime = envVars.TEMPORAL_SHUTDOWN_GRACE_TIME;
|
|
59
72
|
export const grpcProxy = envVars.TEMPORAL_GRPC_PROXY;
|
|
@@ -12,7 +12,9 @@ const CONFIG_KEYS = [
|
|
|
12
12
|
'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS',
|
|
13
13
|
'OUTPUT_WORKER_TELEMETRY_INTERVAL_MS',
|
|
14
14
|
'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS',
|
|
15
|
-
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED'
|
|
15
|
+
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED',
|
|
16
|
+
'TEMPORAL_SHUTDOWN_FORCE_TIME',
|
|
17
|
+
'TEMPORAL_SHUTDOWN_GRACE_TIME'
|
|
16
18
|
];
|
|
17
19
|
|
|
18
20
|
const setEnv = ( overrides = {} ) => {
|
|
@@ -65,6 +67,8 @@ describe( 'worker/configs', () => {
|
|
|
65
67
|
expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
|
|
66
68
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 2 * 60 * 1000 );
|
|
67
69
|
expect( configs.activityHeartbeatEnabled ).toBe( true );
|
|
70
|
+
expect( configs.shutdownForceTime ).toBeUndefined();
|
|
71
|
+
expect( configs.shutdownGraceTime ).toBeUndefined();
|
|
68
72
|
expect( configs.taskQueue ).toBe( 'test-catalog' );
|
|
69
73
|
expect( configs.catalogId ).toBe( 'test-catalog' );
|
|
70
74
|
} );
|
|
@@ -100,6 +104,35 @@ describe( 'worker/configs', () => {
|
|
|
100
104
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 60000 );
|
|
101
105
|
} );
|
|
102
106
|
|
|
107
|
+
it( 'parses Temporal shutdown durations', async () => {
|
|
108
|
+
setEnv( {
|
|
109
|
+
TEMPORAL_SHUTDOWN_FORCE_TIME: '15000',
|
|
110
|
+
TEMPORAL_SHUTDOWN_GRACE_TIME: '15s'
|
|
111
|
+
} );
|
|
112
|
+
const configs = await loadConfigs();
|
|
113
|
+
|
|
114
|
+
expect( configs.shutdownForceTime ).toBe( '15000' );
|
|
115
|
+
expect( configs.shutdownGraceTime ).toBe( '15s' );
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'treats empty Temporal shutdown durations as unset', async () => {
|
|
119
|
+
setEnv( {
|
|
120
|
+
TEMPORAL_SHUTDOWN_FORCE_TIME: '',
|
|
121
|
+
TEMPORAL_SHUTDOWN_GRACE_TIME: ''
|
|
122
|
+
} );
|
|
123
|
+
const configs = await loadConfigs();
|
|
124
|
+
|
|
125
|
+
expect( configs.shutdownForceTime ).toBeUndefined();
|
|
126
|
+
expect( configs.shutdownGraceTime ).toBeUndefined();
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
it( 'throws when Temporal shutdown durations are invalid', async () => {
|
|
130
|
+
setEnv( { TEMPORAL_SHUTDOWN_FORCE_TIME: 'soon' } );
|
|
131
|
+
vi.resetModules();
|
|
132
|
+
|
|
133
|
+
await expect( import( './configs.js' ) ).rejects.toThrow();
|
|
134
|
+
} );
|
|
135
|
+
|
|
103
136
|
it( 'allows zero for worker telemetry interval', async () => {
|
|
104
137
|
setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '0' } );
|
|
105
138
|
const configs = await loadConfigs();
|
|
@@ -2,13 +2,6 @@ import { createChildLogger } from '#logger';
|
|
|
2
2
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
3
|
import { CancellablePromise } from '#utils';
|
|
4
4
|
|
|
5
|
-
const ServingStatus = {
|
|
6
|
-
UNKNOWN: 0,
|
|
7
|
-
SERVING: 1,
|
|
8
|
-
NOT_SERVING: 2,
|
|
9
|
-
SERVICE_UNKNOWN: 3
|
|
10
|
-
};
|
|
11
|
-
|
|
12
5
|
const log = createChildLogger( 'Connection' );
|
|
13
6
|
|
|
14
7
|
export class TemporalConnectionMonitor {
|
|
@@ -28,24 +21,20 @@ export class TemporalConnectionMonitor {
|
|
|
28
21
|
throw new Error( 'Connection health check timed out' );
|
|
29
22
|
} );
|
|
30
23
|
|
|
31
|
-
#healthcheck = async () => this.#connection.
|
|
24
|
+
#healthcheck = async () => this.#connection.workflowService.getSystemInfo( {} );
|
|
32
25
|
|
|
33
26
|
#sleep = async () => delay( this.#CHECK_INTERVAL_MS, 0, { ref: false } );
|
|
34
27
|
|
|
35
28
|
#watch = async () => {
|
|
36
29
|
while ( !this.#cancellation.completed ) {
|
|
37
30
|
try {
|
|
38
|
-
|
|
31
|
+
await Promise.race( [ this.#healthcheck(), this.#getTimeout(), this.#cancellation.promise ] );
|
|
39
32
|
|
|
40
33
|
// cancellation won the race
|
|
41
34
|
if ( this.#cancellation.completed ) {
|
|
42
35
|
break;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
if ( health?.status !== ServingStatus.SERVING ) {
|
|
46
|
-
throw new Error( `Connection not serving (status ${health?.status})` );
|
|
47
|
-
}
|
|
48
|
-
|
|
49
38
|
log.info( this.#failures === 0 ? 'Healthy' : 'Recovered' );
|
|
50
39
|
this.#failures = 0;
|
|
51
40
|
} catch ( error ) {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { TemporalConnectionMonitor } from './connection_monitor.js';
|
|
3
3
|
|
|
4
|
-
const SERVING = 1;
|
|
5
|
-
const NOT_SERVING = 2;
|
|
6
4
|
const CHECK_TIMEOUT_MS = 50;
|
|
7
5
|
const CHECK_INTERVAL_MS = 100;
|
|
8
6
|
|
|
@@ -26,7 +24,7 @@ vi.mock( 'node:timers/promises', () => ( { setTimeout: delayMock } ) );
|
|
|
26
24
|
vi.mock( '#logger', () => ( { createChildLogger: vi.fn( () => mockLogger ) } ) );
|
|
27
25
|
|
|
28
26
|
const createConnection = check => ( {
|
|
29
|
-
|
|
27
|
+
workflowService: { getSystemInfo: check }
|
|
30
28
|
} );
|
|
31
29
|
|
|
32
30
|
const createMonitor = ( check, overrides = {} ) => new TemporalConnectionMonitor( createConnection( check ), {
|
|
@@ -52,8 +50,8 @@ describe( 'TemporalConnectionMonitor', () => {
|
|
|
52
50
|
scheduledDelays.length = 0;
|
|
53
51
|
} );
|
|
54
52
|
|
|
55
|
-
it( 'logs healthy when the
|
|
56
|
-
const check = vi.fn().mockResolvedValue( {
|
|
53
|
+
it( 'logs healthy when the workflow service is reachable', async () => {
|
|
54
|
+
const check = vi.fn().mockResolvedValue( {} );
|
|
57
55
|
const monitor = createMonitor( check );
|
|
58
56
|
|
|
59
57
|
const run = monitor.start();
|
|
@@ -90,7 +88,7 @@ describe( 'TemporalConnectionMonitor', () => {
|
|
|
90
88
|
it( 'logs recovered after a transient failure succeeds', async () => {
|
|
91
89
|
const check = vi.fn()
|
|
92
90
|
.mockRejectedValueOnce( new Error( 'temporary outage' ) )
|
|
93
|
-
.mockResolvedValueOnce( {
|
|
91
|
+
.mockResolvedValueOnce( {} );
|
|
94
92
|
const monitor = createMonitor( check );
|
|
95
93
|
|
|
96
94
|
monitor.start();
|
|
@@ -134,21 +132,6 @@ describe( 'TemporalConnectionMonitor', () => {
|
|
|
134
132
|
expect( monitor.running ).toBe( false );
|
|
135
133
|
} );
|
|
136
134
|
|
|
137
|
-
it( 'treats non-serving health status as a failure', async () => {
|
|
138
|
-
const check = vi.fn().mockResolvedValue( { status: NOT_SERVING } );
|
|
139
|
-
const monitor = createMonitor( check );
|
|
140
|
-
|
|
141
|
-
monitor.start();
|
|
142
|
-
await flushPromises();
|
|
143
|
-
|
|
144
|
-
expect( mockLogger.warn ).toHaveBeenCalledWith( 'Connection unhealthy', {
|
|
145
|
-
error: `Connection not serving (status ${NOT_SERVING})`,
|
|
146
|
-
failures: 1
|
|
147
|
-
} );
|
|
148
|
-
|
|
149
|
-
await monitor.stop();
|
|
150
|
-
} );
|
|
151
|
-
|
|
152
135
|
it( 'returns the same lifecycle promise when started more than once', async () => {
|
|
153
136
|
const check = vi.fn().mockReturnValue( new Promise( () => {} ) );
|
|
154
137
|
const monitor = createMonitor( check );
|
package/src/worker/index.js
CHANGED
|
@@ -33,7 +33,9 @@ const {
|
|
|
33
33
|
maxConcurrentActivityTaskExecutions,
|
|
34
34
|
maxCachedWorkflows,
|
|
35
35
|
maxConcurrentActivityTaskPolls,
|
|
36
|
-
maxConcurrentWorkflowTaskPolls
|
|
36
|
+
maxConcurrentWorkflowTaskPolls,
|
|
37
|
+
shutdownForceTime,
|
|
38
|
+
shutdownGraceTime
|
|
37
39
|
} = configs;
|
|
38
40
|
|
|
39
41
|
const state = {
|
|
@@ -73,6 +75,7 @@ const execute = async () => {
|
|
|
73
75
|
if ( proxy ) {
|
|
74
76
|
log.info( 'Using gRPC proxy', { targetHost: grpcProxy } );
|
|
75
77
|
}
|
|
78
|
+
|
|
76
79
|
state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
|
|
77
80
|
|
|
78
81
|
log.info( 'Creating connection monitor...' );
|
|
@@ -95,7 +98,9 @@ const execute = async () => {
|
|
|
95
98
|
maxCachedWorkflows,
|
|
96
99
|
maxConcurrentActivityTaskPolls,
|
|
97
100
|
maxConcurrentWorkflowTaskPolls,
|
|
98
|
-
bundlerOptions: { webpackConfigHook }
|
|
101
|
+
bundlerOptions: { webpackConfigHook },
|
|
102
|
+
...( shutdownForceTime !== undefined && { shutdownForceTime } ),
|
|
103
|
+
...( shutdownGraceTime !== undefined && { shutdownGraceTime } )
|
|
99
104
|
} );
|
|
100
105
|
|
|
101
106
|
log.info( 'Setting up telemetry...' );
|
package/src/worker/index.spec.js
CHANGED
|
@@ -44,7 +44,9 @@ const {
|
|
|
44
44
|
maxCachedWorkflows: 1000,
|
|
45
45
|
maxConcurrentActivityTaskPolls: 5,
|
|
46
46
|
maxConcurrentWorkflowTaskPolls: 5,
|
|
47
|
-
processFailureShutdownDelay: 0
|
|
47
|
+
processFailureShutdownDelay: 0,
|
|
48
|
+
shutdownForceTime: undefined,
|
|
49
|
+
shutdownGraceTime: undefined
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
const connectionMonitorInstance = {
|
|
@@ -180,6 +182,8 @@ describe( 'worker/index', () => {
|
|
|
180
182
|
resetPromises();
|
|
181
183
|
configValues.apiKey = undefined;
|
|
182
184
|
configValues.grpcProxy = undefined;
|
|
185
|
+
configValues.shutdownForceTime = undefined;
|
|
186
|
+
configValues.shutdownGraceTime = undefined;
|
|
183
187
|
catalogJobInstance.error = null;
|
|
184
188
|
catalogJobInstance.errorCb = null;
|
|
185
189
|
catalogJobInstance.running = false;
|
|
@@ -237,6 +241,8 @@ describe( 'worker/index', () => {
|
|
|
237
241
|
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
238
242
|
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
239
243
|
} ) );
|
|
244
|
+
expect( Worker.create.mock.calls[0][0] ).not.toHaveProperty( 'shutdownForceTime' );
|
|
245
|
+
expect( Worker.create.mock.calls[0][0] ).not.toHaveProperty( 'shutdownGraceTime' );
|
|
240
246
|
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
|
|
241
247
|
expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
|
|
242
248
|
expect( setupInterruptionHandlerMock ).toHaveBeenCalledWith( expect.any( Function ) );
|
|
@@ -264,6 +270,21 @@ describe( 'worker/index', () => {
|
|
|
264
270
|
await settleWorker();
|
|
265
271
|
} );
|
|
266
272
|
|
|
273
|
+
it( 'passes configured shutdown durations to the worker', async () => {
|
|
274
|
+
configValues.shutdownForceTime = '30s';
|
|
275
|
+
configValues.shutdownGraceTime = '10s';
|
|
276
|
+
const { Worker } = await import( '@temporalio/worker' );
|
|
277
|
+
|
|
278
|
+
await importWorker();
|
|
279
|
+
|
|
280
|
+
await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
281
|
+
shutdownForceTime: '30s',
|
|
282
|
+
shutdownGraceTime: '10s'
|
|
283
|
+
} ) ) );
|
|
284
|
+
|
|
285
|
+
await settleWorker();
|
|
286
|
+
} );
|
|
287
|
+
|
|
267
288
|
it( 'runs graceful shutdown when interrupted', async () => {
|
|
268
289
|
await importWorker();
|
|
269
290
|
|
|
@@ -252,7 +252,7 @@ export const hashSourceCode = async rootDir => {
|
|
|
252
252
|
try {
|
|
253
253
|
const { hash } = await hashElement( rootDir, {
|
|
254
254
|
folders: {
|
|
255
|
-
exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test', 'logs', '
|
|
255
|
+
exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test', 'logs', 'temp' ],
|
|
256
256
|
ignoreRootName: true
|
|
257
257
|
},
|
|
258
258
|
files: {
|
|
@@ -574,7 +574,7 @@ describe( 'hashSourceCode', () => {
|
|
|
574
574
|
|
|
575
575
|
// The cruft tree is identical source plus large excluded artifacts that
|
|
576
576
|
// boot must not walk: local trace dumps under logs/ and build output under dist/.
|
|
577
|
-
for ( const excluded of [ 'logs', 'logs/runs', '
|
|
577
|
+
for ( const excluded of [ 'logs', 'logs/runs', 'node_modules' ] ) {
|
|
578
578
|
const dir = join( withCruft, excluded );
|
|
579
579
|
mkdirSync( dir, { recursive: true } );
|
|
580
580
|
writeFileSync( join( dir, 'dump.json' ), JSON.stringify( { blob: 'x'.repeat( 50_000 ) } ) );
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Info } from '@temporalio/activity';
|
|
2
|
-
/**
|
|
3
|
-
* Context returned by {@link getContext} when running inside a Temporal Activity (step or evaluator).
|
|
4
|
-
*/
|
|
5
|
-
export type Context = {
|
|
6
|
-
/** Temporal info about the current activity */
|
|
7
|
-
activityInfo: Info,
|
|
8
|
-
/** Path of the workflow file */
|
|
9
|
-
workflowFilename: string
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Returns information about the current Temporal execution.
|
|
14
|
-
*
|
|
15
|
-
* Only available when called from within a step or evaluator (Temporal Activities) running in the Temporal runtime.
|
|
16
|
-
*
|
|
17
|
-
* @remarks
|
|
18
|
-
* - Returns `null` when not called inside a Temporal Activity (steps/evaluators);
|
|
19
|
-
* - Returns `null` when not called from within a running Temporal worker, like in unit tests environment;
|
|
20
|
-
*
|
|
21
|
-
* @returns The workflow context, or `null` if unavailable or incomplete.
|
|
22
|
-
*/
|
|
23
|
-
export declare function getExecutionContext(): Context | null;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Storage } from '#async_storage';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Returns information trapped on AsyncStorage about the workflow invoking an activity
|
|
5
|
-
*
|
|
6
|
-
* @returns {object}
|
|
7
|
-
*/
|
|
8
|
-
export const getExecutionContext = () => {
|
|
9
|
-
const ctx = Storage.load();
|
|
10
|
-
if ( !ctx ) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
workflowFilename: ctx.workflowFilename,
|
|
16
|
-
activityInfo: ctx.activityInfo
|
|
17
|
-
};
|
|
18
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { messageBus } from '#bus';
|
|
3
|
-
import { emitEvent } from './events.js';
|
|
4
|
-
|
|
5
|
-
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
-
|
|
7
|
-
describe( 'eventId integration', () => {
|
|
8
|
-
beforeEach( () => {
|
|
9
|
-
messageBus.removeAllListeners();
|
|
10
|
-
} );
|
|
11
|
-
|
|
12
|
-
it( 'stamps a UUID v4 eventId on every emit (end-to-end via messageBus)', () => {
|
|
13
|
-
const handler = vi.fn();
|
|
14
|
-
messageBus.on( 'external:cost:llm:request', handler );
|
|
15
|
-
|
|
16
|
-
emitEvent( 'cost:llm:request', { modelId: 'gpt-4o' } );
|
|
17
|
-
|
|
18
|
-
expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
19
|
-
eventId: expect.stringMatching( UUID_V4_REGEX ),
|
|
20
|
-
modelId: 'gpt-4o'
|
|
21
|
-
} ) );
|
|
22
|
-
} );
|
|
23
|
-
|
|
24
|
-
it( 'cost:http:request and http:request for the same fetch get distinct eventIds', () => {
|
|
25
|
-
const costHandler = vi.fn();
|
|
26
|
-
const reqHandler = vi.fn();
|
|
27
|
-
messageBus.on( 'external:cost:http:request', costHandler );
|
|
28
|
-
messageBus.on( 'external:http:request', reqHandler );
|
|
29
|
-
|
|
30
|
-
const sharedRequestId = 'req-xyz';
|
|
31
|
-
emitEvent( 'cost:http:request', { requestId: sharedRequestId, url: 'https://x.test', cost: 1 } );
|
|
32
|
-
emitEvent( 'http:request', { requestId: sharedRequestId, url: 'https://x.test', status: 200 } );
|
|
33
|
-
|
|
34
|
-
const costEventId = costHandler.mock.calls[0][0].eventId;
|
|
35
|
-
const reqEventId = reqHandler.mock.calls[0][0].eventId;
|
|
36
|
-
expect( costEventId ).toMatch( UUID_V4_REGEX );
|
|
37
|
-
expect( reqEventId ).toMatch( UUID_V4_REGEX );
|
|
38
|
-
expect( costEventId ).not.toBe( reqEventId );
|
|
39
|
-
} );
|
|
40
|
-
|
|
41
|
-
it( 'honors a caller-supplied eventId end-to-end', () => {
|
|
42
|
-
const handler = vi.fn();
|
|
43
|
-
messageBus.on( 'external:custom:event', handler );
|
|
44
|
-
|
|
45
|
-
emitEvent( 'custom:event', { eventId: 'fixed-id-123', payload: 'hi' } );
|
|
46
|
-
|
|
47
|
-
expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
48
|
-
eventId: 'fixed-id-123',
|
|
49
|
-
payload: 'hi'
|
|
50
|
-
} ) );
|
|
51
|
-
} );
|
|
52
|
-
} );
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Emits a custom event on the in-process message bus.
|
|
3
|
-
*
|
|
4
|
-
* When called inside an Output activity context, the framework automatically
|
|
5
|
-
* attaches `activityInfo`, `workflowDetails`, and `outputActivityKind` onto the emitted payload.
|
|
6
|
-
*
|
|
7
|
-
* @param eventName - The name of the event to emit
|
|
8
|
-
* @param payload - An optional payload to send to the event
|
|
9
|
-
*/
|
|
10
|
-
export declare function emitEvent( eventName: string, payload?: unknown ): void;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { messageBus } from '#bus';
|
|
2
|
-
import { Storage } from '#async_storage';
|
|
3
|
-
|
|
4
|
-
export const emitEvent = ( eventName, payload ) => {
|
|
5
|
-
const ctx = Storage.load();
|
|
6
|
-
|
|
7
|
-
messageBus.emit( `external:${eventName}`, {
|
|
8
|
-
...payload ?? {},
|
|
9
|
-
...( ctx && {
|
|
10
|
-
activityInfo: ctx.activityInfo,
|
|
11
|
-
workflowDetails: ctx.workflowDetails,
|
|
12
|
-
outputActivityKind: ctx.outputActivityKind
|
|
13
|
-
} )
|
|
14
|
-
} );
|
|
15
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* > [!WARNING]
|
|
3
|
-
* > **Internal use only.** Not part of the public API; may change without notice.
|
|
4
|
-
*
|
|
5
|
-
* @packageDocumentation
|
|
6
|
-
*/
|
|
7
|
-
export { getExecutionContext } from './context';
|
|
8
|
-
export { emitEvent } from './events';
|
|
9
|
-
export * as Tracing from './tracing';
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { Attribute } from '#trace_attribute';
|
|
2
|
-
|
|
3
|
-
export { Attribute } from '#trace_attribute';
|
|
4
|
-
/**
|
|
5
|
-
* Creates a new event.
|
|
6
|
-
*
|
|
7
|
-
* @param args
|
|
8
|
-
* @param args.id - A unique id for the Event.
|
|
9
|
-
* @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
|
|
10
|
-
* @param args.name - The human-friendly name of the Event: query, request, create.
|
|
11
|
-
* @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
12
|
-
*/
|
|
13
|
-
export declare function addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Concludes an event.
|
|
17
|
-
*
|
|
18
|
-
* @param args
|
|
19
|
-
* @param args.id - The id of the event to conclude.
|
|
20
|
-
* @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
|
|
21
|
-
*/
|
|
22
|
-
export declare function addEventEnd( args: { id: string; details: unknown } ): void;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Concludes an event with an error.
|
|
26
|
-
*
|
|
27
|
-
* @param args
|
|
28
|
-
* @param args.id - The id of the event to conclude.
|
|
29
|
-
* @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
|
|
30
|
-
*/
|
|
31
|
-
export declare function addEventError( args: { id: string; details: unknown } ): void;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Adds an attribute to an event.
|
|
35
|
-
*
|
|
36
|
-
* @param args
|
|
37
|
-
* @param args.eventId - The id of the event to attach the attribute to.
|
|
38
|
-
* @param args.attribute - The attribute to attach to the event.
|
|
39
|
-
*/
|
|
40
|
-
export declare function addEventAttribute( args: { eventId: string; attribute: Attribute.Instance } ): void;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { addEventActionWithContext, EventAction } from '#tracing';
|
|
2
|
-
|
|
3
|
-
export { Attribute } from '#trace_attribute';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a new event.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} args
|
|
9
|
-
* @param {string} args.id - A unique id for the Event.
|
|
10
|
-
* @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
|
|
11
|
-
* @param {string} args.name - The human-friendly name of the Event: query, request, create.
|
|
12
|
-
* @param {object} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
|
|
13
|
-
* @returns {void}
|
|
14
|
-
*/
|
|
15
|
-
export const addEventStart = ( { id, kind, name, details } ) =>
|
|
16
|
-
addEventActionWithContext( EventAction.START, { kind, name, details, id } );
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Concludes an event.
|
|
20
|
-
*
|
|
21
|
-
* @param {object} args
|
|
22
|
-
* @param {string} args.id - The id of the event to conclude.
|
|
23
|
-
* @param {object} args.details - Arbitrary data to add to this event, it will be used as the "output" field.
|
|
24
|
-
* @returns {void}
|
|
25
|
-
*/
|
|
26
|
-
export const addEventEnd = ( { id, details } ) => addEventActionWithContext( EventAction.END, { id, details } );
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Concludes an event with an error.
|
|
30
|
-
*
|
|
31
|
-
* @param {object} args
|
|
32
|
-
* @param {string} args.id - The id of the event to conclude.
|
|
33
|
-
* @param {object} args.details - Arbitrary data to add to this event, it will be used as the "error" field.
|
|
34
|
-
* @returns {void}
|
|
35
|
-
*/
|
|
36
|
-
export const addEventError = ( { id, details } ) => addEventActionWithContext( EventAction.ERROR, { id, details } );
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Adds an attribute to an event.
|
|
40
|
-
*
|
|
41
|
-
* @param {object} args
|
|
42
|
-
* @param {string} args.eventId - The id of the event to attach the attribute to.
|
|
43
|
-
* @param {string} args.name - The attribute name
|
|
44
|
-
* @param {unknown} args.value - The attribute value
|
|
45
|
-
* @returns {void}
|
|
46
|
-
*/
|
|
47
|
-
export const addEventAttribute = ( { eventId, attribute } ) =>
|
|
48
|
-
addEventActionWithContext( EventAction.ADD_ATTR, { id: eventId, details: attribute } );
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import * as stackTraceParser from 'stacktrace-parser';
|
|
2
|
-
|
|
3
|
-
// OS separator, but in a deterministic way, allowing this to work in Temporal's sandbox
|
|
4
|
-
// This avoids importing from node:path
|
|
5
|
-
const SEP = new Error().stack.includes( '/' ) ? '/' : '\\';
|
|
6
|
-
|
|
7
|
-
const transformSeparators = path => path.replaceAll( '/', SEP );
|
|
8
|
-
const defaultIgnorePaths = [
|
|
9
|
-
'/@outputai/core/',
|
|
10
|
-
'/@outputai/llm/',
|
|
11
|
-
'/@outputai/evals/',
|
|
12
|
-
'/sdk/core/',
|
|
13
|
-
'/sdk/llm/',
|
|
14
|
-
'/sdk/evals/',
|
|
15
|
-
'node:internal/',
|
|
16
|
-
'evalmachine.',
|
|
17
|
-
'webpack/bootstrap'
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Return the directory of the file invoking the code that called this function
|
|
22
|
-
* Excludes some internal paths and the sdk itself
|
|
23
|
-
*/
|
|
24
|
-
export default ( additionalIgnorePaths = [] ) => {
|
|
25
|
-
const stack = new Error().stack;
|
|
26
|
-
const lines = stackTraceParser.parse( stack );
|
|
27
|
-
const ignorePaths = [ ...additionalIgnorePaths, ...defaultIgnorePaths ].map( transformSeparators );
|
|
28
|
-
|
|
29
|
-
const frame = lines.find( l => !ignorePaths.some( p => l.file.includes( p ) ) );
|
|
30
|
-
if ( !frame ) {
|
|
31
|
-
throw new Error( `Invocation dir resolution via stack trace failed. Stack: ${stack}` );
|
|
32
|
-
}
|
|
33
|
-
return frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
|
|
34
|
-
};
|