@outputai/core 0.5.2-next.b54869d.0 → 0.5.2-next.cc8a372.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.5.2-next.b54869d.0",
3
+ "version": "0.5.2-next.cc8a372.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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() } );