@output.ai/core 0.5.10 → 0.5.11-dev.pr421-1996c13

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.
@@ -5,9 +5,9 @@ const connectionMock = { close: closeMock };
5
5
 
6
6
  const queryMock = vi.fn();
7
7
  const getHandleMock = vi.fn().mockReturnValue( { query: queryMock } );
8
- const ClientMock = vi.fn().mockImplementation( () => ( {
9
- workflow: { getHandle: getHandleMock }
10
- } ) );
8
+ const ClientMock = vi.fn().mockImplementation( function () {
9
+ return { workflow: { getHandle: getHandleMock } };
10
+ } );
11
11
  const connectMock = vi.fn().mockResolvedValue( connectionMock );
12
12
 
13
13
  vi.mock( '@temporalio/client', () => ( {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/core",
3
- "version": "0.5.10",
3
+ "version": "0.5.11-dev.pr421-1996c13",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,6 +19,10 @@
19
19
  "./hooks": {
20
20
  "types": "./src/hooks/index.d.ts",
21
21
  "import": "./src/hooks/index.js"
22
+ },
23
+ "./context": {
24
+ "types": "./src/context/index.d.ts",
25
+ "import": "./src/context/index.js"
22
26
  }
23
27
  },
24
28
  "files": [
@@ -31,21 +35,21 @@
31
35
  "output-healthcheck": "./bin/healthcheck.mjs"
32
36
  },
33
37
  "dependencies": {
34
- "@aws-sdk/client-s3": "3.913.0",
35
- "@babel/generator": "7.28.3",
36
- "@babel/parser": "7.28.4",
37
- "@babel/traverse": "7.28.4",
38
- "@babel/types": "7.28.4",
39
- "@temporalio/activity": "1.13.1",
40
- "@temporalio/client": "1.13.1",
41
- "@temporalio/common": "1.13.1",
42
- "@temporalio/worker": "1.13.1",
43
- "@temporalio/workflow": "1.13.1",
44
- "redis": "5.8.3",
38
+ "@aws-sdk/client-s3": "3.1000.0",
39
+ "@babel/generator": "7.29.1",
40
+ "@babel/parser": "7.29.0",
41
+ "@babel/traverse": "7.29.0",
42
+ "@babel/types": "7.29.0",
43
+ "@temporalio/activity": "1.15.0",
44
+ "@temporalio/client": "1.15.0",
45
+ "@temporalio/common": "1.15.0",
46
+ "@temporalio/worker": "1.15.0",
47
+ "@temporalio/workflow": "1.15.0",
48
+ "redis": "5.11.0",
45
49
  "stacktrace-parser": "0.1.11",
46
- "undici": "7.18.2",
47
- "winston": "3.17.0",
48
- "zod": "^4.3.6"
50
+ "undici": "7.22.0",
51
+ "winston": "3.19.0",
52
+ "zod": "4.3.6"
49
53
  },
50
54
  "license": "Apache-2.0",
51
55
  "publishConfig": {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Context returned by {@link getContext} when running inside a Temporal Activity (step or evaluator).
3
+ */
4
+ export type Context = {
5
+ /** Information about the current workflow execution */
6
+ workflow: {
7
+ /** Temporal's workflow execution id */
8
+ id: string;
9
+ /** Workflow name (Temporal's workflow "type" value) */
10
+ name: string;
11
+ /** Path of the workflow file */
12
+ filename: string;
13
+ }
14
+ };
15
+
16
+ /**
17
+ * Returns information about the current Temporal execution.
18
+ *
19
+ * Only available when called from within a step or evaluator (Temporal Activities) running in the Temporal runtime.
20
+ *
21
+ * @remarks
22
+ * - Returns `null` when not called inside a Temporal Activity (steps/evaluators);
23
+ * - Returns `null` when not called from within a running Temporal worker, like in unit tests environment;
24
+ *
25
+ * @returns The workflow context, or `null` if unavailable or incomplete.
26
+ */
27
+ export declare function getContext(): Context | null;
@@ -0,0 +1,17 @@
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 getContext = () => {
9
+ const ctx = Storage.load();
10
+
11
+ if ( !ctx?.executionContext || !ctx?.workflowFilename ) {
12
+ return null;
13
+ }
14
+
15
+ const { workflowId: id, workflowName: name } = ctx.executionContext;
16
+ return { workflow: { id, name, filename: ctx.workflowFilename } };
17
+ };
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const loadMock = vi.fn();
4
+ vi.mock( '#async_storage', () => ( {
5
+ Storage: { load: loadMock }
6
+ } ) );
7
+
8
+ describe( 'context/getContext', () => {
9
+ beforeEach( () => {
10
+ vi.clearAllMocks();
11
+ vi.resetModules();
12
+ } );
13
+
14
+ it( 'returns null when no context is stored', async () => {
15
+ loadMock.mockReturnValue( undefined );
16
+ const { getContext } = await import( './index.js' );
17
+ expect( getContext() ).toBeNull();
18
+ } );
19
+
20
+ it( 'returns null when executionContext is missing', async () => {
21
+ loadMock.mockReturnValue( { workflowFilename: '/workflows/foo.js' } );
22
+ const { getContext } = await import( './index.js' );
23
+ expect( getContext() ).toBeNull();
24
+ } );
25
+
26
+ it( 'returns null when workflowFilename is missing', async () => {
27
+ loadMock.mockReturnValue( { executionContext: { workflowId: 'wf-1', workflowName: 'myWorkflow' } } );
28
+ const { getContext } = await import( './index.js' );
29
+ expect( getContext() ).toBeNull();
30
+ } );
31
+
32
+ it( 'returns workflow context when storage has full context', async () => {
33
+ loadMock.mockReturnValue( {
34
+ executionContext: { workflowId: 'wf-1', workflowName: 'myWorkflow' },
35
+ workflowFilename: '/workflows/myWorkflow.js'
36
+ } );
37
+ const { getContext } = await import( './index.js' );
38
+ expect( getContext() ).toEqual( {
39
+ workflow: { id: 'wf-1', name: 'myWorkflow', filename: '/workflows/myWorkflow.js' }
40
+ } );
41
+ } );
42
+ } );
@@ -59,7 +59,7 @@ const callerDir = process.argv[2];
59
59
  workflowsPath,
60
60
  activities,
61
61
  sinks,
62
- interceptors: initInterceptors( { activities } ),
62
+ interceptors: initInterceptors( { activities, workflows } ),
63
63
  maxConcurrentWorkflowTaskExecutions,
64
64
  maxConcurrentActivityTaskExecutions,
65
65
  maxCachedWorkflows,
@@ -112,7 +112,7 @@ describe( 'worker/index', () => {
112
112
  maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
113
113
  maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
114
114
  } ) );
115
- expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {} } );
115
+ expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
116
116
  expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
117
117
  expect( startCatalogMock ).toHaveBeenCalledWith( {
118
118
  connection: mockConnection,
@@ -140,6 +140,7 @@ describe( 'worker/index', () => {
140
140
  apiKey: 'secret'
141
141
  } ) );
142
142
  } );
143
+ runState.resolve();
143
144
  await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
144
145
  } );
145
146
 
@@ -25,8 +25,9 @@ const log = createChildLogger( 'Activity' );
25
25
  - Headers injected by the workflow interceptor (executionContext)
26
26
  */
27
27
  export class ActivityExecutionInterceptor {
28
- constructor( activities ) {
28
+ constructor( { activities, workflows } ) {
29
29
  this.activities = activities;
30
+ this.workflowsMap = workflows.reduce( ( map, w ) => map.set( w.name, w ), new Map() );
30
31
  };
31
32
 
32
33
  async execute( input, next ) {
@@ -46,7 +47,12 @@ export class ActivityExecutionInterceptor {
46
47
  // Sends heartbeat to communicate that activity is still alive
47
48
  intervals.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
48
49
 
49
- const output = await Storage.runWithContext( async _ => next( input ), { parentId: activityId, executionContext } );
50
+ // Wraps the execution with accessible metadata for the activity
51
+ const output = await Storage.runWithContext( async _ => next( input ), {
52
+ parentId: activityId,
53
+ executionContext,
54
+ workflowFilename: this.workflowsMap.get( workflowName ).path
55
+ } );
50
56
 
51
57
  log.info( `Ended ${activityType} ${kind}`, { event: LifecycleEvent.END, kind, ...logContext, durationMs: Date.now() - startDate } );
52
58
  addEventEnd( { details: output, ...traceArguments } );
@@ -3,10 +3,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
3
  const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
4
4
 
5
5
  const heartbeatMock = vi.fn();
6
+ const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
6
7
  const contextInfoMock = {
7
8
  workflowExecution: { workflowId: 'wf-1' },
8
9
  activityId: 'act-1',
9
- activityType: 'myWorkflow#myStep'
10
+ activityType: 'myWorkflow#myStep',
11
+ workflowType: 'myWorkflow'
10
12
  };
11
13
 
12
14
  vi.mock( '@temporalio/activity', () => ( {
@@ -20,7 +22,7 @@ vi.mock( '@temporalio/activity', () => ( {
20
22
 
21
23
  vi.mock( '#async_storage', () => ( {
22
24
  Storage: {
23
- runWithContext: async fn => fn()
25
+ runWithContext: runWithContextMock
24
26
  }
25
27
  } ) );
26
28
 
@@ -59,6 +61,8 @@ const makeActivities = () => ( {
59
61
  'myWorkflow#myStep': { [METADATA_ACCESS_SYMBOL]: { type: 'step' } }
60
62
  } );
61
63
 
64
+ const makeWorkflows = () => [ { name: 'myWorkflow', path: '/workflows/myWorkflow.js' } ];
65
+
62
66
  const makeInput = () => ( {
63
67
  args: [ { someInput: 'data' } ],
64
68
  headers: {}
@@ -81,7 +85,7 @@ describe( 'ActivityExecutionInterceptor', () => {
81
85
 
82
86
  it( 'records trace start and end events on successful execution', async () => {
83
87
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
84
- const interceptor = new ActivityExecutionInterceptor( makeActivities() );
88
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
85
89
  const next = vi.fn().mockResolvedValue( { result: 'ok' } );
86
90
 
87
91
  const promise = interceptor.execute( makeInput(), next );
@@ -92,11 +96,19 @@ describe( 'ActivityExecutionInterceptor', () => {
92
96
  expect( addEventStartMock ).toHaveBeenCalledOnce();
93
97
  expect( addEventEndMock ).toHaveBeenCalledOnce();
94
98
  expect( addEventErrorMock ).not.toHaveBeenCalled();
99
+ expect( runWithContextMock ).toHaveBeenCalledWith(
100
+ expect.any( Function ),
101
+ {
102
+ parentId: 'act-1',
103
+ executionContext: { workflowId: 'wf-1' },
104
+ workflowFilename: '/workflows/myWorkflow.js'
105
+ }
106
+ );
95
107
  } );
96
108
 
97
109
  it( 'records trace error event on failed execution', async () => {
98
110
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
99
- const interceptor = new ActivityExecutionInterceptor( makeActivities() );
111
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
100
112
  const error = new Error( 'step failed' );
101
113
  const next = vi.fn().mockRejectedValue( error );
102
114
 
@@ -111,7 +123,7 @@ describe( 'ActivityExecutionInterceptor', () => {
111
123
 
112
124
  it( 'sends periodic heartbeats during activity execution', async () => {
113
125
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
114
- const interceptor = new ActivityExecutionInterceptor( makeActivities() );
126
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
115
127
 
116
128
  // next() resolves only after we manually resolve it, simulating a long-running activity
117
129
  const deferred = { resolve: null };
@@ -138,7 +150,7 @@ describe( 'ActivityExecutionInterceptor', () => {
138
150
 
139
151
  it( 'clears heartbeat interval after activity completes', async () => {
140
152
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
141
- const interceptor = new ActivityExecutionInterceptor( makeActivities() );
153
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
142
154
  const next = vi.fn().mockResolvedValue( { result: 'ok' } );
143
155
 
144
156
  const promise = interceptor.execute( makeInput(), next );
@@ -152,7 +164,7 @@ describe( 'ActivityExecutionInterceptor', () => {
152
164
 
153
165
  it( 'clears heartbeat interval after activity fails', async () => {
154
166
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
155
- const interceptor = new ActivityExecutionInterceptor( makeActivities() );
167
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
156
168
  const next = vi.fn().mockRejectedValue( new Error( 'boom' ) );
157
169
 
158
170
  const promise = interceptor.execute( makeInput(), next );
@@ -167,7 +179,7 @@ describe( 'ActivityExecutionInterceptor', () => {
167
179
  it( 'does not heartbeat when OUTPUT_ACTIVITY_HEARTBEAT_ENABLED is false', async () => {
168
180
  vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED', 'false' );
169
181
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
170
- const interceptor = new ActivityExecutionInterceptor( makeActivities() );
182
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
171
183
 
172
184
  const deferred = { resolve: null };
173
185
  const next = vi.fn().mockImplementation( () => new Promise( r => {
@@ -4,7 +4,7 @@ import { ActivityExecutionInterceptor } from './interceptors/activity.js';
4
4
 
5
5
  const __dirname = dirname( fileURLToPath( import.meta.url ) );
6
6
 
7
- export const initInterceptors = ( { activities } ) => ( {
7
+ export const initInterceptors = ( { activities, workflows } ) => ( {
8
8
  workflowModules: [ join( __dirname, './interceptors/workflow.js' ) ],
9
- activityInbound: [ () => new ActivityExecutionInterceptor( activities ) ]
9
+ activityInbound: [ () => new ActivityExecutionInterceptor( { activities, workflows } ) ]
10
10
  } );
@@ -17,12 +17,14 @@ vi.mock( '@temporalio/client', async importOriginal => {
17
17
  const actual = await importOriginal();
18
18
  return {
19
19
  ...actual,
20
- Client: vi.fn().mockImplementation( () => ( {
21
- workflow: {
22
- start: workflowStartMock,
23
- getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
24
- }
25
- } ) )
20
+ Client: vi.fn().mockImplementation( function () {
21
+ return {
22
+ workflow: {
23
+ start: workflowStartMock,
24
+ getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
25
+ }
26
+ };
27
+ } )
26
28
  };
27
29
  } );
28
30