@outputai/core 0.5.2-next.b54869d.0 → 0.5.2
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
package/src/worker/configs.js
CHANGED
|
@@ -25,6 +25,11 @@ const envVarSchema = z.object( {
|
|
|
25
25
|
OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 2 * 60 * 1000 ) ), // 2min
|
|
26
26
|
// Whether to send activity heartbeats (enabled by default)
|
|
27
27
|
OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: z.transform( v => v === undefined ? true : isStringboolTrue( v ) ),
|
|
28
|
+
// When true, activities fire Temporal signals carrying attribute/event data (LLM usage,
|
|
29
|
+
// HTTP request count/cost) back to the workflow for aggregation in the result.
|
|
30
|
+
// Defaulted OFF: the current emission architecture bloats Temporal history.
|
|
31
|
+
// Set to "true" to opt in to per-event attribute collection and aggregations.
|
|
32
|
+
OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION: z.transform( v => v === undefined ? false : isStringboolTrue( v ) ),
|
|
28
33
|
// Time to allow for hooks to flush before shutdown
|
|
29
34
|
OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) ),
|
|
30
35
|
// HTTP CONNECT proxy for Temporal gRPC connections (e.g. "proxy-host:8080").
|
|
@@ -53,5 +58,6 @@ export const taskQueue = envVars.OUTPUT_CATALOG_ID;
|
|
|
53
58
|
export const catalogId = envVars.OUTPUT_CATALOG_ID;
|
|
54
59
|
export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
|
|
55
60
|
export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
|
|
61
|
+
export const enableAttributeSignalEmission = envVars.OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION;
|
|
56
62
|
export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
|
|
57
63
|
export const grpcProxy = envVars.TEMPORAL_GRPC_PROXY;
|
|
@@ -11,7 +11,8 @@ const CONFIG_KEYS = [
|
|
|
11
11
|
'TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS',
|
|
12
12
|
'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS',
|
|
13
13
|
'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS',
|
|
14
|
-
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED'
|
|
14
|
+
'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED',
|
|
15
|
+
'OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION'
|
|
15
16
|
];
|
|
16
17
|
|
|
17
18
|
const setEnv = ( overrides = {} ) => {
|
|
@@ -63,6 +64,7 @@ describe( 'worker/configs', () => {
|
|
|
63
64
|
expect( configs.maxConcurrentWorkflowTaskPolls ).toBe( 5 );
|
|
64
65
|
expect( configs.activityHeartbeatIntervalMs ).toBe( 2 * 60 * 1000 );
|
|
65
66
|
expect( configs.activityHeartbeatEnabled ).toBe( true );
|
|
67
|
+
expect( configs.enableAttributeSignalEmission ).toBe( false );
|
|
66
68
|
expect( configs.taskQueue ).toBe( 'test-catalog' );
|
|
67
69
|
expect( configs.catalogId ).toBe( 'test-catalog' );
|
|
68
70
|
} );
|
|
@@ -120,6 +122,30 @@ describe( 'worker/configs', () => {
|
|
|
120
122
|
expect( configsDefault.activityHeartbeatEnabled ).toBe( true );
|
|
121
123
|
} );
|
|
122
124
|
|
|
125
|
+
it( 'OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION: "true"|"1"|"on" → true', async () => {
|
|
126
|
+
for ( const val of [ 'true', '1', 'on' ] ) {
|
|
127
|
+
setEnv( { OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION: val } );
|
|
128
|
+
const configs = await loadConfigs();
|
|
129
|
+
expect( configs.enableAttributeSignalEmission ).toBe( true );
|
|
130
|
+
clearEnv();
|
|
131
|
+
}
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION: "false"|other → false, undefined → false (default off)', async () => {
|
|
135
|
+
setEnv( { OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION: 'false' } );
|
|
136
|
+
const configsFalse = await loadConfigs();
|
|
137
|
+
expect( configsFalse.enableAttributeSignalEmission ).toBe( false );
|
|
138
|
+
|
|
139
|
+
setEnv( { OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION: '0' } );
|
|
140
|
+
const configsZero = await loadConfigs();
|
|
141
|
+
expect( configsZero.enableAttributeSignalEmission ).toBe( false );
|
|
142
|
+
|
|
143
|
+
clearEnv();
|
|
144
|
+
setEnv(); // only OUTPUT_CATALOG_ID; OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION absent → default false
|
|
145
|
+
const configsDefault = await loadConfigs();
|
|
146
|
+
expect( configsDefault.enableAttributeSignalEmission ).toBe( false );
|
|
147
|
+
} );
|
|
148
|
+
|
|
123
149
|
it( 'parses TEMPORAL_ADDRESS and TEMPORAL_NAMESPACE', async () => {
|
|
124
150
|
setEnv( { TEMPORAL_ADDRESS: 'temporal:7233', TEMPORAL_NAMESPACE: 'my-ns' } );
|
|
125
151
|
const configs = await loadConfigs();
|
|
@@ -3,7 +3,7 @@ import { Storage } from '#async_storage';
|
|
|
3
3
|
import * as Tracing from '#tracing';
|
|
4
4
|
import { headersToObject } from '../sandboxed_utils.js';
|
|
5
5
|
import { BusEventType, METADATA_ACCESS_SYMBOL, Signal } from '#consts';
|
|
6
|
-
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs, namespace } from '../configs.js';
|
|
6
|
+
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs, enableAttributeSignalEmission, namespace } from '../configs.js';
|
|
7
7
|
import { messageBus } from '#bus';
|
|
8
8
|
import { Client } from '@temporalio/client';
|
|
9
9
|
import { createChildLogger } from '#logger';
|
|
@@ -81,6 +81,9 @@ export class ActivityExecutionInterceptor {
|
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
const sendAttributeSignal = attribute => {
|
|
84
|
+
if ( !enableAttributeSignalEmission ) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
84
87
|
attribute.setActivity( id, name );
|
|
85
88
|
state.signals.push(
|
|
86
89
|
workflowHandle
|
|
@@ -85,6 +85,9 @@ vi.mock( '../configs.js', () => ( {
|
|
|
85
85
|
get activityHeartbeatIntervalMs() {
|
|
86
86
|
return parseInt( process.env.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS || '120000', 10 );
|
|
87
87
|
},
|
|
88
|
+
get enableAttributeSignalEmission() {
|
|
89
|
+
return process.env.OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION === 'true';
|
|
90
|
+
},
|
|
88
91
|
get namespace() {
|
|
89
92
|
return process.env.TEMPORAL_NAMESPACE || 'default';
|
|
90
93
|
}
|
|
@@ -111,6 +114,8 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
111
114
|
// Default: heartbeat enabled with 50ms interval for fast tests
|
|
112
115
|
vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED', 'true' );
|
|
113
116
|
vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS', '50' );
|
|
117
|
+
// Default: attribute signal emission enabled so existing tests can verify signal-sending behaviour
|
|
118
|
+
vi.stubEnv( 'OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION', 'true' );
|
|
114
119
|
} );
|
|
115
120
|
|
|
116
121
|
afterEach( () => {
|
|
@@ -219,6 +224,24 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
219
224
|
expect( allSettledWithTimeoutMock ).toHaveBeenCalledWith( [ expect.any( Promise ) ], 30_000 );
|
|
220
225
|
} );
|
|
221
226
|
|
|
227
|
+
it( 'does not signal when OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION is false', async () => {
|
|
228
|
+
vi.stubEnv( 'OUTPUT_ENABLE_ATTRIBUTE_SIGNAL_EMISSION', 'false' );
|
|
229
|
+
const attribute = { setActivity: vi.fn() };
|
|
230
|
+
runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
|
|
231
|
+
ctx.sendAttributeSignal( attribute );
|
|
232
|
+
return fn();
|
|
233
|
+
} );
|
|
234
|
+
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
235
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
236
|
+
const next = vi.fn().mockResolvedValue( { result: 'ok' } );
|
|
237
|
+
|
|
238
|
+
await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( { result: 'ok' } );
|
|
239
|
+
|
|
240
|
+
expect( attribute.setActivity ).not.toHaveBeenCalled();
|
|
241
|
+
expect( workflowHandleMock.signal ).not.toHaveBeenCalled();
|
|
242
|
+
expect( allSettledWithTimeoutMock ).toHaveBeenCalledWith( [], 30_000 );
|
|
243
|
+
} );
|
|
244
|
+
|
|
222
245
|
it( 'records trace error event on failed execution', async () => {
|
|
223
246
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
224
247
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|