@maxdrellin/xenocline 0.0.7 → 0.0.8

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.
@@ -2,15 +2,44 @@ import { Input } from '../input';
2
2
  import { Context } from '../context';
3
3
  import { Output } from '../output';
4
4
  import { Transition } from './transition';
5
+ /**
6
+ * Transform function for connections between nodes.
7
+ * Takes the output from the source node and the current context,
8
+ * and returns the input for the target node along with an updated context.
9
+ *
10
+ * CONCURRENCY WARNING: In parallel execution scenarios (fan-out from one node
11
+ * to multiple target nodes), each connection's transform receives the same
12
+ * source context. Context mutations from one connection may be overwritten
13
+ * by another. The last transform to complete will have its context changes
14
+ * reflected in the process state.
15
+ *
16
+ * Best practice: Minimize context mutations in transforms, or use unique
17
+ * context keys per connection path to avoid conflicts.
18
+ */
5
19
  export type TransformFunction<O extends Output = Output, C extends Context = Context> = (output: O, context: C) => Promise<[Input, C]>;
6
20
  export interface Connection<O extends Output = Output, C extends Context = Context> extends Transition {
7
21
  type: 'connection';
8
22
  targetNodeId: string;
23
+ /**
24
+ * Optional function to transform the output of the current phase
25
+ * to the input of the target phase.
26
+ * If not provided, the output is assumed to be compatible directly.
27
+ *
28
+ * See TransformFunction documentation for concurrency considerations.
29
+ */
9
30
  transform: TransformFunction<O, C>;
10
31
  }
11
32
  export interface ConnectionOptions<O extends Output = Output, C extends Context = Context> {
12
33
  transform: TransformFunction<O, C>;
13
34
  }
35
+ /**
36
+ * Default connection options with a pass-through transform.
37
+ * WARNING: The default transform uses a type assertion (output as Input) which provides
38
+ * no runtime validation. Users should provide their own transform function if the output
39
+ * type does not structurally match the expected input type of the target node.
40
+ * The type assertion is used here to maintain backward compatibility and allow simple
41
+ * pass-through scenarios where Output and Input have compatible shapes.
42
+ */
14
43
  export declare const DEFAULT_CONNECTION_OPTIONS: ConnectionOptions;
15
44
  export declare const createConnection: <O extends Output = Output, C extends Context = Context>(id: string, targetNodeId: string, options?: Partial<ConnectionOptions<O, C>>) => Readonly<Connection<O, C>>;
16
45
  export declare const isConnection: <O extends Output = Output, C extends Context = Context>(item: any) => item is Connection<O, C>;
@@ -1,8 +1,18 @@
1
1
  import { createTransition, isTransition, validateTransition } from './transition.js';
2
2
  import { clean } from '../util/general.js';
3
3
 
4
- const DEFAULT_CONNECTION_OPTIONS = {
4
+ /**
5
+ * Default connection options with a pass-through transform.
6
+ * WARNING: The default transform uses a type assertion (output as Input) which provides
7
+ * no runtime validation. Users should provide their own transform function if the output
8
+ * type does not structurally match the expected input type of the target node.
9
+ * The type assertion is used here to maintain backward compatibility and allow simple
10
+ * pass-through scenarios where Output and Input have compatible shapes.
11
+ */ const DEFAULT_CONNECTION_OPTIONS = {
5
12
  transform: async (output, context)=>{
13
+ // Type assertion used here - Output and Input are both extensible objects
14
+ // so this is safe for pass-through scenarios, but users should validate
15
+ // or transform data if types don't align
6
16
  return [
7
17
  output,
8
18
  context
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","sources":["../../src/transition/connection.ts"],"sourcesContent":["import { Input } from '../input';\nimport { Context } from '../context';\nimport { Output } from '../output';\nimport { createTransition, isTransition, Transition, validateTransition } from './transition';\nimport { clean } from '../util/general';\n\nexport type TransformFunction<O extends Output = Output, C extends Context = Context> = (output: O, context: C) => Promise<[Input, C]>;\n\n// MODIFIED: Connection to extend Transition\nexport interface Connection<\n O extends Output = Output,\n C extends Context = Context,\n> extends Transition {\n type: 'connection';\n targetNodeId: string; // ID of the target PhaseNode in the process's phases collection\n // Optional function to transform the output of the current phase\n // to the input of the target phase.\n // If not provided, the output is assumed to be compatible directly.\n transform: TransformFunction<O, C>;\n}\n\nexport interface ConnectionOptions<O extends Output = Output, C extends Context = Context> {\n transform: TransformFunction<O, C>;\n}\n\nexport const DEFAULT_CONNECTION_OPTIONS: ConnectionOptions = {\n transform: async (output, context) => {\n return [output as Input, context];\n }\n};\n\nexport const createConnection = <O extends Output = Output, C extends Context = Context>(\n id: string,\n targetNodeId: string,\n options?: Partial<ConnectionOptions<O, C>>\n): Readonly<Connection<O, C>> => {\n\n let connectionOptions: ConnectionOptions<O, C> = { ...DEFAULT_CONNECTION_OPTIONS } as unknown as ConnectionOptions<O, C>;\n\n if (options) {\n connectionOptions = { ...connectionOptions, ...clean(options) };\n }\n\n return {\n ...createTransition('connection', id),\n targetNodeId,\n transform: connectionOptions.transform,\n } as Connection<O, C>;\n};\n\nexport const isConnection = <O extends Output = Output, C extends Context = Context>(item: any): item is Connection<O, C> => {\n return isTransition(item) && item.type === 'connection' && (item as Connection<O, C>).targetNodeId !== undefined;\n};\n\nexport const validateConnection = (\n item: any,\n coordinates?: string[]\n): Array<{ coordinates: string[], error: string }> => {\n const errors: Array<{ coordinates: string[], error: string }> = [];\n const connectionBaseCoordinates = [...(coordinates || []), 'Connection'];\n\n errors.push(...validateTransition(item, coordinates));\n\n if (errors.length === 0) {\n if (item && typeof item === 'object') {\n const connectionSpecificErrorPath = [...connectionBaseCoordinates, `Connection: ${item.id}`];\n\n if (item.type === 'connection') {\n if (typeof item.targetNodeId !== 'string') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"targetNodeId\" must be a string when type is \"connection\".' });\n }\n // transform is optional, but if present, must be a function.\n if (item.transform !== undefined && typeof item.transform !== 'function') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Optional property \"transform\" must be a function if present.' });\n }\n } else {\n // If type is not 'connection', but these properties exist and are malformed.\n // This primarily helps catch if a non-connection object has these fields incorrectly defined.\n if (item.targetNodeId !== undefined && typeof item.targetNodeId !== 'string') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"targetNodeId\" is present but is not a string.' });\n }\n if (item.transform !== undefined && typeof item.transform !== 'function') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"transform\" is present but is not a function.' });\n }\n }\n }\n }\n return errors;\n};\n\n/**\n * Event emitted specifically for connection transitions.\n */\nexport interface ConnectionEvent extends TransitionEvent {\n transitionType: 'connection';\n}\n"],"names":["DEFAULT_CONNECTION_OPTIONS","transform","output","context","createConnection","id","targetNodeId","options","connectionOptions","clean","createTransition","isConnection","item","isTransition","type","undefined","validateConnection","coordinates","errors","connectionBaseCoordinates","push","validateTransition","length","connectionSpecificErrorPath","error"],"mappings":";;;MAyBaA,0BAAAA,GAAgD;AACzDC,IAAAA,SAAAA,EAAW,OAAOC,MAAAA,EAAQC,OAAAA,GAAAA;QACtB,OAAO;AAACD,YAAAA,MAAAA;AAAiBC,YAAAA;AAAQ,SAAA;AACrC,IAAA;AACJ;AAEO,MAAMC,gBAAAA,GAAmB,CAC5BC,EAAAA,EACAC,YAAAA,EACAC,OAAAA,GAAAA;AAGA,IAAA,IAAIC,iBAAAA,GAA6C;AAAE,QAAA,GAAGR;AAA2B,KAAA;AAEjF,IAAA,IAAIO,OAAAA,EAAS;QACTC,iBAAAA,GAAoB;AAAE,YAAA,GAAGA,iBAAiB;AAAE,YAAA,GAAGC,MAAMF,OAAAA;AAAS,SAAA;AAClE,IAAA;IAEA,OAAO;QACH,GAAGG,gBAAAA,CAAiB,cAAcL,EAAAA,CAAG;AACrCC,QAAAA,YAAAA;AACAL,QAAAA,SAAAA,EAAWO,kBAAkBP;AACjC,KAAA;AACJ;AAEO,MAAMU,eAAe,CAAyDC,IAAAA,GAAAA;IACjF,OAAOC,YAAAA,CAAaD,SAASA,IAAAA,CAAKE,IAAI,KAAK,YAAA,IAAiBF,IAAAA,CAA0BN,YAAY,KAAKS,SAAAA;AAC3G;AAEO,MAAMC,kBAAAA,GAAqB,CAC9BJ,IAAAA,EACAK,WAAAA,GAAAA;AAEA,IAAA,MAAMC,SAA0D,EAAE;AAClE,IAAA,MAAMC,yBAAAA,GAA4B;AAAKF,QAAAA,GAAAA,WAAAA,IAAe,EAAE;AAAG,QAAA;AAAa,KAAA;IAExEC,MAAAA,CAAOE,IAAI,CAAA,GAAIC,kBAAAA,CAAmBT,IAAAA,EAAMK,WAAAA,CAAAA,CAAAA;IAExC,IAAIC,MAAAA,CAAOI,MAAM,KAAK,CAAA,EAAG;QACrB,IAAIV,IAAAA,IAAQ,OAAOA,IAAAA,KAAS,QAAA,EAAU;AAClC,YAAA,MAAMW,2BAAAA,GAA8B;AAAIJ,gBAAAA,GAAAA,yBAAAA;AAA2B,gBAAA,CAAC,YAAY,EAAEP,IAAAA,CAAKP,EAAE,CAAA;AAAG,aAAA;YAE5F,IAAIO,IAAAA,CAAKE,IAAI,KAAK,YAAA,EAAc;AAC5B,gBAAA,IAAI,OAAOF,IAAAA,CAAKN,YAAY,KAAK,QAAA,EAAU;AACvCY,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAAsE,qBAAA,CAAA;AACzI,gBAAA;;gBAEA,IAAIZ,IAAAA,CAAKX,SAAS,KAAKc,SAAAA,IAAa,OAAOH,IAAAA,CAAKX,SAAS,KAAK,UAAA,EAAY;AACtEiB,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAA+D,qBAAA,CAAA;AAClI,gBAAA;YACJ,CAAA,MAAO;;;gBAGH,IAAIZ,IAAAA,CAAKN,YAAY,KAAKS,SAAAA,IAAa,OAAOH,IAAAA,CAAKN,YAAY,KAAK,QAAA,EAAU;AAC1EY,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAA0D,qBAAA,CAAA;AAC7H,gBAAA;gBACA,IAAIZ,IAAAA,CAAKX,SAAS,KAAKc,SAAAA,IAAa,OAAOH,IAAAA,CAAKX,SAAS,KAAK,UAAA,EAAY;AACtEiB,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAAyD,qBAAA,CAAA;AAC5H,gBAAA;AACJ,YAAA;AACJ,QAAA;AACJ,IAAA;IACA,OAAON,MAAAA;AACX;;;;"}
1
+ {"version":3,"file":"connection.js","sources":["../../src/transition/connection.ts"],"sourcesContent":["import { Input } from '../input';\nimport { Context } from '../context';\nimport { Output } from '../output';\nimport { createTransition, isTransition, Transition, validateTransition } from './transition';\nimport { clean } from '../util/general';\n\n/**\n * Transform function for connections between nodes.\n * Takes the output from the source node and the current context,\n * and returns the input for the target node along with an updated context.\n *\n * CONCURRENCY WARNING: In parallel execution scenarios (fan-out from one node\n * to multiple target nodes), each connection's transform receives the same\n * source context. Context mutations from one connection may be overwritten\n * by another. The last transform to complete will have its context changes\n * reflected in the process state.\n *\n * Best practice: Minimize context mutations in transforms, or use unique\n * context keys per connection path to avoid conflicts.\n */\nexport type TransformFunction<O extends Output = Output, C extends Context = Context> = (output: O, context: C) => Promise<[Input, C]>;\n\n// MODIFIED: Connection to extend Transition\nexport interface Connection<\n O extends Output = Output,\n C extends Context = Context,\n> extends Transition {\n type: 'connection';\n targetNodeId: string; // ID of the target PhaseNode in the process's phases collection\n /**\n * Optional function to transform the output of the current phase\n * to the input of the target phase.\n * If not provided, the output is assumed to be compatible directly.\n *\n * See TransformFunction documentation for concurrency considerations.\n */\n transform: TransformFunction<O, C>;\n}\n\nexport interface ConnectionOptions<O extends Output = Output, C extends Context = Context> {\n transform: TransformFunction<O, C>;\n}\n\n/**\n * Default connection options with a pass-through transform.\n * WARNING: The default transform uses a type assertion (output as Input) which provides\n * no runtime validation. Users should provide their own transform function if the output\n * type does not structurally match the expected input type of the target node.\n * The type assertion is used here to maintain backward compatibility and allow simple\n * pass-through scenarios where Output and Input have compatible shapes.\n */\nexport const DEFAULT_CONNECTION_OPTIONS: ConnectionOptions = {\n transform: async (output, context) => {\n // Type assertion used here - Output and Input are both extensible objects\n // so this is safe for pass-through scenarios, but users should validate\n // or transform data if types don't align\n return [output as Input, context];\n }\n};\n\nexport const createConnection = <O extends Output = Output, C extends Context = Context>(\n id: string,\n targetNodeId: string,\n options?: Partial<ConnectionOptions<O, C>>\n): Readonly<Connection<O, C>> => {\n\n let connectionOptions: ConnectionOptions<O, C> = { ...DEFAULT_CONNECTION_OPTIONS } as unknown as ConnectionOptions<O, C>;\n\n if (options) {\n connectionOptions = { ...connectionOptions, ...clean(options) };\n }\n\n return {\n ...createTransition('connection', id),\n targetNodeId,\n transform: connectionOptions.transform,\n } as Connection<O, C>;\n};\n\nexport const isConnection = <O extends Output = Output, C extends Context = Context>(item: any): item is Connection<O, C> => {\n return isTransition(item) && item.type === 'connection' && (item as Connection<O, C>).targetNodeId !== undefined;\n};\n\nexport const validateConnection = (\n item: any,\n coordinates?: string[]\n): Array<{ coordinates: string[], error: string }> => {\n const errors: Array<{ coordinates: string[], error: string }> = [];\n const connectionBaseCoordinates = [...(coordinates || []), 'Connection'];\n\n errors.push(...validateTransition(item, coordinates));\n\n if (errors.length === 0) {\n if (item && typeof item === 'object') {\n const connectionSpecificErrorPath = [...connectionBaseCoordinates, `Connection: ${item.id}`];\n\n if (item.type === 'connection') {\n if (typeof item.targetNodeId !== 'string') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"targetNodeId\" must be a string when type is \"connection\".' });\n }\n // transform is optional, but if present, must be a function.\n if (item.transform !== undefined && typeof item.transform !== 'function') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Optional property \"transform\" must be a function if present.' });\n }\n } else {\n // If type is not 'connection', but these properties exist and are malformed.\n // This primarily helps catch if a non-connection object has these fields incorrectly defined.\n if (item.targetNodeId !== undefined && typeof item.targetNodeId !== 'string') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"targetNodeId\" is present but is not a string.' });\n }\n if (item.transform !== undefined && typeof item.transform !== 'function') {\n errors.push({ coordinates: connectionSpecificErrorPath, error: 'Property \"transform\" is present but is not a function.' });\n }\n }\n }\n }\n return errors;\n};\n\n/**\n * Event emitted specifically for connection transitions.\n */\nexport interface ConnectionEvent extends TransitionEvent {\n transitionType: 'connection';\n}\n"],"names":["DEFAULT_CONNECTION_OPTIONS","transform","output","context","createConnection","id","targetNodeId","options","connectionOptions","clean","createTransition","isConnection","item","isTransition","type","undefined","validateConnection","coordinates","errors","connectionBaseCoordinates","push","validateTransition","length","connectionSpecificErrorPath","error"],"mappings":";;;AA2CA;;;;;;;UAQaA,0BAAAA,GAAgD;AACzDC,IAAAA,SAAAA,EAAW,OAAOC,MAAAA,EAAQC,OAAAA,GAAAA;;;;QAItB,OAAO;AAACD,YAAAA,MAAAA;AAAiBC,YAAAA;AAAQ,SAAA;AACrC,IAAA;AACJ;AAEO,MAAMC,gBAAAA,GAAmB,CAC5BC,EAAAA,EACAC,YAAAA,EACAC,OAAAA,GAAAA;AAGA,IAAA,IAAIC,iBAAAA,GAA6C;AAAE,QAAA,GAAGR;AAA2B,KAAA;AAEjF,IAAA,IAAIO,OAAAA,EAAS;QACTC,iBAAAA,GAAoB;AAAE,YAAA,GAAGA,iBAAiB;AAAE,YAAA,GAAGC,MAAMF,OAAAA;AAAS,SAAA;AAClE,IAAA;IAEA,OAAO;QACH,GAAGG,gBAAAA,CAAiB,cAAcL,EAAAA,CAAG;AACrCC,QAAAA,YAAAA;AACAL,QAAAA,SAAAA,EAAWO,kBAAkBP;AACjC,KAAA;AACJ;AAEO,MAAMU,eAAe,CAAyDC,IAAAA,GAAAA;IACjF,OAAOC,YAAAA,CAAaD,SAASA,IAAAA,CAAKE,IAAI,KAAK,YAAA,IAAiBF,IAAAA,CAA0BN,YAAY,KAAKS,SAAAA;AAC3G;AAEO,MAAMC,kBAAAA,GAAqB,CAC9BJ,IAAAA,EACAK,WAAAA,GAAAA;AAEA,IAAA,MAAMC,SAA0D,EAAE;AAClE,IAAA,MAAMC,yBAAAA,GAA4B;AAAKF,QAAAA,GAAAA,WAAAA,IAAe,EAAE;AAAG,QAAA;AAAa,KAAA;IAExEC,MAAAA,CAAOE,IAAI,CAAA,GAAIC,kBAAAA,CAAmBT,IAAAA,EAAMK,WAAAA,CAAAA,CAAAA;IAExC,IAAIC,MAAAA,CAAOI,MAAM,KAAK,CAAA,EAAG;QACrB,IAAIV,IAAAA,IAAQ,OAAOA,IAAAA,KAAS,QAAA,EAAU;AAClC,YAAA,MAAMW,2BAAAA,GAA8B;AAAIJ,gBAAAA,GAAAA,yBAAAA;AAA2B,gBAAA,CAAC,YAAY,EAAEP,IAAAA,CAAKP,EAAE,CAAA;AAAG,aAAA;YAE5F,IAAIO,IAAAA,CAAKE,IAAI,KAAK,YAAA,EAAc;AAC5B,gBAAA,IAAI,OAAOF,IAAAA,CAAKN,YAAY,KAAK,QAAA,EAAU;AACvCY,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAAsE,qBAAA,CAAA;AACzI,gBAAA;;gBAEA,IAAIZ,IAAAA,CAAKX,SAAS,KAAKc,SAAAA,IAAa,OAAOH,IAAAA,CAAKX,SAAS,KAAK,UAAA,EAAY;AACtEiB,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAA+D,qBAAA,CAAA;AAClI,gBAAA;YACJ,CAAA,MAAO;;;gBAGH,IAAIZ,IAAAA,CAAKN,YAAY,KAAKS,SAAAA,IAAa,OAAOH,IAAAA,CAAKN,YAAY,KAAK,QAAA,EAAU;AAC1EY,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAA0D,qBAAA,CAAA;AAC7H,gBAAA;gBACA,IAAIZ,IAAAA,CAAKX,SAAS,KAAKc,SAAAA,IAAa,OAAOH,IAAAA,CAAKX,SAAS,KAAK,UAAA,EAAY;AACtEiB,oBAAAA,MAAAA,CAAOE,IAAI,CAAC;wBAAEH,WAAAA,EAAaM,2BAAAA;wBAA6BC,KAAAA,EAAO;AAAyD,qBAAA,CAAA;AAC5H,gBAAA;AACJ,YAAA;AACJ,QAAA;AACJ,IAAA;IACA,OAAON,MAAAA;AACX;;;;"}
@@ -183,8 +183,18 @@ const validateTermination = (item, coordinates)=>{
183
183
  return errors;
184
184
  };
185
185
 
186
- const DEFAULT_CONNECTION_OPTIONS = {
186
+ /**
187
+ * Default connection options with a pass-through transform.
188
+ * WARNING: The default transform uses a type assertion (output as Input) which provides
189
+ * no runtime validation. Users should provide their own transform function if the output
190
+ * type does not structurally match the expected input type of the target node.
191
+ * The type assertion is used here to maintain backward compatibility and allow simple
192
+ * pass-through scenarios where Output and Input have compatible shapes.
193
+ */ const DEFAULT_CONNECTION_OPTIONS = {
187
194
  transform: async (output, context)=>{
195
+ // Type assertion used here - Output and Input are both extensible objects
196
+ // so this is safe for pass-through scenarios, but users should validate
197
+ // or transform data if types don't align
188
198
  return [
189
199
  output,
190
200
  context
@@ -883,17 +893,26 @@ async function handleNextStep(nodeOutput, nodeId, next, state) {
883
893
  await handleNextStep(nodeOutput, decision.id, decisionOutcome, state); // outcome processed, decision.id is context for next step if it's an error source. The original nodeId is implicitly the source of this decision.
884
894
  await dispatchEvent(state.eventState, createDecisionEvent(nodeId, 'end', decision), state.context);
885
895
  } catch (decisionError) {
896
+ const errorMessage = `Decision error on '${decision.id}' from node '${nodeId}': ${decisionError.message}`;
886
897
  // eslint-disable-next-line no-console
887
- console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR] Error in decision ${decision.id} from node ${nodeId}:`, {
898
+ console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR]`, {
899
+ error: errorMessage,
888
900
  decisionError,
889
- nodeId,
890
- decisionId: decision.id
901
+ decisionId: decision.id,
902
+ sourceNodeId: nodeId
891
903
  });
892
904
  state.errors.push({
893
905
  nodeId: decision.id,
894
- message: decisionError.message
906
+ message: errorMessage,
907
+ details: {
908
+ sourceNodeId: nodeId,
909
+ originalError: decisionError.message
910
+ }
895
911
  });
896
- // Note: 'end' event for this decision path is skipped due to error.
912
+ // Note: Decision errors are logged but do not halt the process.
913
+ // This allows other parallel decisions to continue executing.
914
+ // If you need decision errors to halt execution, consider throwing here
915
+ // or checking state.errors after process completion.
897
916
  }
898
917
  }
899
918
  } else if (Array.isArray(next) && next.length > 0 && next.every(isConnection)) {
@@ -919,15 +938,24 @@ async function handleNextStep(nodeOutput, nodeId, next, state) {
919
938
  state.context = nextContext;
920
939
  //console.log('[_HANDLE_NEXT_STEP_CONNECTION_TRANSFORM_SUCCESS]', { nodeId, targetNodeId: connection.targetNodeId, nextInput, nextContext });
921
940
  } catch (transformError) {
941
+ const errorMessage = `Transform error on connection '${connection.id}' from node '${nodeId}' to '${connection.targetNodeId}': ${transformError.message}`;
922
942
  // eslint-disable-next-line no-console
923
- console.error(`[_HANDLE_NEXT_STEP_CONNECTION_TRANSFORM_ERROR] Error in transform for connection from ${nodeId} to ${connection.targetNodeId}:`, {
943
+ console.error(`[_HANDLE_NEXT_STEP_CONNECTION_TRANSFORM_ERROR]`, {
944
+ error: errorMessage,
924
945
  transformError,
925
- nodeId,
946
+ connectionId: connection.id,
947
+ sourceNodeId: nodeId,
926
948
  targetNodeId: connection.targetNodeId
927
949
  });
950
+ // Store error with connection ID for better traceability
928
951
  state.errors.push({
929
- nodeId: connection.targetNodeId,
930
- message: transformError.message
952
+ nodeId: connection.id,
953
+ message: errorMessage,
954
+ details: {
955
+ sourceNodeId: nodeId,
956
+ targetNodeId: connection.targetNodeId,
957
+ originalError: transformError.message
958
+ }
931
959
  });
932
960
  continue;
933
961
  }
@@ -947,18 +975,21 @@ async function handleNextStep(nodeOutput, nodeId, next, state) {
947
975
  const result = nodeOutput;
948
976
  if (termination.terminate) {
949
977
  //console.log('[_HANDLE_NEXT_STEP_TERMINATION_CALLING_TERMINATE_FN]', { nodeId, terminationId: termination.id });
950
- termination.terminate(nodeOutput, state.context);
978
+ await termination.terminate(nodeOutput, state.context);
951
979
  await dispatchEvent(state.eventState, createTerminationEvent(nodeId, 'terminate', termination, {
952
980
  output: nodeOutput
953
981
  }), state.context);
954
982
  }
955
983
  state.results[termination.id] = result;
956
984
  } else if (Array.isArray(next) && next.length === 0) {
957
- // Empty array, potentially from a Decision that leads to no connections or an empty Connection array.
958
- // This could mean an implicit termination for this path or simply no further action from this branch.
959
- // If it's considered an end state for the path, store the result.
985
+ // Empty array from a Decision means no path should be taken from this node.
986
+ // This is treated as an implicit termination, storing the result with a generated key.
987
+ // Note: This behavior means a Decision can dynamically terminate a process path.
960
988
  const result = nodeOutput;
961
- state.results[nodeId] = result; // Using nodeId as the key for this implicit termination
989
+ const implicitTerminationId = `${nodeId}_implicit_end`;
990
+ state.results[implicitTerminationId] = result;
991
+ // eslint-disable-next-line no-console
992
+ console.warn(`[_HANDLE_NEXT_STEP_IMPLICIT_TERMINATION] Node ${nodeId} received empty next array, treating as implicit termination with id: ${implicitTerminationId}`);
962
993
  } else {
963
994
  // If there is no next (e.g. next is undefined or null after a decision), or it's an unhandled type.
964
995
  // Consider this an end state and store the result with the nodeId
@@ -1093,15 +1124,21 @@ async function executeNode(nodeId, input, state) {
1093
1124
  await handleNextStep(output, decision.id, decisionOutcome, state);
1094
1125
  dispatchEvent(state.eventState, createDecisionEvent(nodeId, 'end', decision), state.context);
1095
1126
  } catch (decisionError) {
1127
+ const errorMessage = `Decision error on '${decision.id}' for node '${nodeId}': ${decisionError.message}`;
1096
1128
  // eslint-disable-next-line no-console
1097
- console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR] Error in decision ${decision.id} for node ${nodeId}:`, {
1129
+ console.error(`[_HANDLE_NEXT_STEP_DECISION_ERROR]`, {
1130
+ error: errorMessage,
1098
1131
  decisionError,
1099
- nodeId,
1100
- decisionId: decision.id
1132
+ decisionId: decision.id,
1133
+ sourceNodeId: nodeId
1101
1134
  });
1102
1135
  state.errors.push({
1103
1136
  nodeId: decision.id,
1104
- message: decisionError.message
1137
+ message: errorMessage,
1138
+ details: {
1139
+ sourceNodeId: nodeId,
1140
+ originalError: decisionError.message
1141
+ }
1105
1142
  });
1106
1143
  }
1107
1144
  })();
@@ -1142,6 +1179,14 @@ async function executeNode(nodeId, input, state) {
1142
1179
  nodeId,
1143
1180
  message: error.message
1144
1181
  });
1182
+ // Clean up any pending aggregator deferred on error
1183
+ if (state.aggregatorDeferreds.has(nodeId)) {
1184
+ const deferred = state.aggregatorDeferreds.get(nodeId);
1185
+ if (deferred) {
1186
+ deferred.reject(error);
1187
+ }
1188
+ state.aggregatorDeferreds.delete(nodeId);
1189
+ }
1145
1190
  throw error;
1146
1191
  } finally{
1147
1192
  //console.log('[EXECUTE_NODE_RECURSIVE_IIFE_FINALLY]', { nodeId, hasAggregatorDeferred: state.aggregatorDeferreds.has(nodeId) });
@@ -1232,13 +1277,31 @@ async function executeProcess(processInstance, beginning, options) {
1232
1277
  collectedErrors: state.errors
1233
1278
  });
1234
1279
  }
1280
+ // Check for and reject any pending aggregators that never completed
1235
1281
  if (state.aggregatorDeferreds && state.aggregatorDeferreds.size > 0) {
1236
1282
  const pendingNodeIds = state.pendingAggregatorIds ? state.pendingAggregatorIds().join(', ') : 'unknown';
1237
1283
  // eslint-disable-next-line no-console
1238
- console.warn(`[EXECUTE_PROCESS_PENDING_AGGREGATORS] Process execution may have pending aggregators: ${pendingNodeIds}.`, {
1284
+ console.warn(`[EXECUTE_PROCESS_PENDING_AGGREGATORS] Process execution completed with pending aggregators: ${pendingNodeIds}. These will be rejected.`, {
1239
1285
  processName: processInstance.name,
1240
1286
  pendingNodeIds
1241
1287
  });
1288
+ // Reject all pending aggregators to prevent hanging promises
1289
+ for (const nodeId of state.aggregatorDeferreds.keys()){
1290
+ const deferred = state.aggregatorDeferreds.get(nodeId);
1291
+ if (deferred) {
1292
+ const error = new Error(`Aggregator node '${nodeId}' did not receive all expected inputs before process completion. This may indicate a process design issue where not all paths leading to the aggregator were executed.`);
1293
+ deferred.reject(error);
1294
+ state.errors.push({
1295
+ nodeId,
1296
+ message: error.message,
1297
+ details: {
1298
+ reason: 'incomplete_aggregation'
1299
+ }
1300
+ });
1301
+ }
1302
+ }
1303
+ // Clear the map after rejecting all
1304
+ state.aggregatorDeferreds.clear();
1242
1305
  }
1243
1306
  dispatchEvent(state.eventState, createProcessEvent(processInstance.name, 'end', processInstance, {
1244
1307
  input: processExecutionOptions.input,