@prompd/cli 0.4.4 → 0.4.6

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.
@@ -1239,6 +1239,103 @@ async function executePromptNode(node, context, options, trace, state, workflowF
1239
1239
  },
1240
1240
  };
1241
1241
  await emitAgentCheckpoint(afterEvent, options, workflowFile);
1242
+ // Apply guardrail validation if configured
1243
+ if (data.guardrail) {
1244
+ const guardrail = data.guardrail;
1245
+ let isRejected = false;
1246
+ // Evaluate rejection condition
1247
+ if (guardrail.rejectionExpression) {
1248
+ // Advanced: Custom rejection expression
1249
+ try {
1250
+ // Extend context with response in nodeOutputs for guardrail expression evaluation
1251
+ const guardContext = {
1252
+ nodeOutputs: { ...context.nodeOutputs, response: result },
1253
+ variables: context.variables,
1254
+ workflow: context.workflow,
1255
+ previous_output: context.previous_output,
1256
+ };
1257
+ const evalResult = evaluateExpression(guardrail.rejectionExpression, guardContext);
1258
+ isRejected = Boolean(evalResult);
1259
+ }
1260
+ catch (error) {
1261
+ addTraceEntry(trace, {
1262
+ type: 'debug_step',
1263
+ nodeId: node.id,
1264
+ nodeName: data.label,
1265
+ message: `Guardrail expression evaluation failed: ${error instanceof Error ? error.message : String(error)}`,
1266
+ }, options);
1267
+ isRejected = false; // Default to not rejected on expression error
1268
+ }
1269
+ }
1270
+ else if (guardrail.rejectionField && guardrail.expectedFormat === 'json') {
1271
+ // Simple: Field-based check for JSON responses
1272
+ try {
1273
+ const parsed = typeof result === 'string' ? JSON.parse(result) : result;
1274
+ const fieldValue = parsed?.[guardrail.rejectionField];
1275
+ if (guardrail.passWhen === 'true') {
1276
+ // Pass when field is true, reject when false
1277
+ isRejected = !fieldValue;
1278
+ }
1279
+ else if (guardrail.passWhen === 'false') {
1280
+ // Pass when field is false, reject when true
1281
+ isRejected = Boolean(fieldValue);
1282
+ }
1283
+ addTraceEntry(trace, {
1284
+ type: 'debug_step',
1285
+ nodeId: node.id,
1286
+ nodeName: data.label,
1287
+ message: `Guardrail field check: ${guardrail.rejectionField}=${fieldValue}, passWhen=${guardrail.passWhen}, rejected=${isRejected}`,
1288
+ }, options);
1289
+ }
1290
+ catch (error) {
1291
+ addTraceEntry(trace, {
1292
+ type: 'debug_step',
1293
+ nodeId: node.id,
1294
+ nodeName: data.label,
1295
+ message: `Guardrail JSON parse failed: ${error instanceof Error ? error.message : String(error)}`,
1296
+ }, options);
1297
+ isRejected = false; // Default to not rejected on parse error
1298
+ }
1299
+ }
1300
+ // Handle rejection
1301
+ if (isRejected) {
1302
+ const failAction = guardrail.failAction || 'error';
1303
+ const rejectMessage = guardrail.customRejectMessage || 'Content rejected by guardrail';
1304
+ addTraceEntry(trace, {
1305
+ type: 'debug_step',
1306
+ nodeId: node.id,
1307
+ nodeName: data.label,
1308
+ message: `Guardrail REJECTED content, failAction=${failAction}`,
1309
+ data: { rejectMessage, result },
1310
+ }, options);
1311
+ if (failAction === 'error' || failAction === 'stop') {
1312
+ throw new Error(rejectMessage);
1313
+ }
1314
+ else if (failAction === 'continue') {
1315
+ // Return reject message and continue workflow
1316
+ return rejectMessage;
1317
+ }
1318
+ }
1319
+ else {
1320
+ // Guardrail passed - determine output based on outputMode
1321
+ const outputMode = guardrail.outputMode || 'passthrough';
1322
+ addTraceEntry(trace, {
1323
+ type: 'debug_step',
1324
+ nodeId: node.id,
1325
+ nodeName: data.label,
1326
+ message: `Guardrail PASSED content, outputMode=${outputMode}`,
1327
+ }, options);
1328
+ if (outputMode === 'original') {
1329
+ // Return the original input that was sent to the guardrail prompt
1330
+ result = context.previous_output;
1331
+ }
1332
+ else if (outputMode === 'reject-message') {
1333
+ // Return custom message even on pass (useful for custom routing)
1334
+ result = guardrail.customRejectMessage || result;
1335
+ }
1336
+ // 'passthrough' - keep result as-is (default)
1337
+ }
1338
+ }
1242
1339
  // Apply output mapping if defined
1243
1340
  if (data.outputMapping) {
1244
1341
  const mapped = {};
@@ -3863,8 +3960,36 @@ Analyze the input above. Return a JSON object:
3863
3960
  if (jsonMatch) {
3864
3961
  try {
3865
3962
  const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
3866
- // If guardrailPassExpression is configured, use it to evaluate the result
3867
- if (data.guardrailPassExpression) {
3963
+ // Priority 1: Custom rejection expression (new field)
3964
+ if (data.guardrailRejectionExpression) {
3965
+ const exprContext = { ...context, response: parsed, previous_output: parsed, input: parsed };
3966
+ const exprResult = evaluateExpression(data.guardrailRejectionExpression, exprContext);
3967
+ guardrailResult = {
3968
+ rejected: Boolean(exprResult), // Expression returns true for REJECT
3969
+ score: typeof parsed.score === 'number' ? parsed.score : undefined,
3970
+ analysis: parsed.analysis,
3971
+ };
3972
+ }
3973
+ // Priority 2: Field-based check (new fields)
3974
+ else if (data.guardrailRejectionField && data.guardrailExpectedFormat === 'json') {
3975
+ const fieldValue = parsed?.[data.guardrailRejectionField];
3976
+ if (data.guardrailPassWhen === 'true') {
3977
+ // Pass when field is true, reject when false
3978
+ guardrailResult.rejected = !fieldValue;
3979
+ }
3980
+ else if (data.guardrailPassWhen === 'false') {
3981
+ // Pass when field is false, reject when true
3982
+ guardrailResult.rejected = Boolean(fieldValue);
3983
+ }
3984
+ else {
3985
+ // Default: check parsed.rejected field
3986
+ guardrailResult.rejected = parsed.rejected === true;
3987
+ }
3988
+ guardrailResult.score = typeof parsed.score === 'number' ? parsed.score : undefined;
3989
+ guardrailResult.analysis = parsed.analysis;
3990
+ }
3991
+ // Priority 3: Legacy passExpression
3992
+ else if (data.guardrailPassExpression) {
3868
3993
  const exprContext = { ...context, previous_output: parsed, input: parsed };
3869
3994
  const exprResult = evaluateExpression(data.guardrailPassExpression, exprContext);
3870
3995
  guardrailResult = {
@@ -3873,8 +3998,8 @@ Analyze the input above. Return a JSON object:
3873
3998
  analysis: parsed.analysis,
3874
3999
  };
3875
4000
  }
4001
+ // Priority 4: Fallback to checking parsed.rejected field
3876
4002
  else {
3877
- // Fallback to checking parsed.rejected field
3878
4003
  guardrailResult = {
3879
4004
  rejected: parsed.rejected === true,
3880
4005
  score: typeof parsed.score === 'number' ? parsed.score : undefined,
@@ -3884,16 +4009,22 @@ Analyze the input above. Return a JSON object:
3884
4009
  }
3885
4010
  catch {
3886
4011
  // If JSON parsing fails, try expression evaluation with raw response
3887
- if (data.guardrailPassExpression) {
4012
+ if (data.guardrailRejectionExpression) {
4013
+ const exprContext = { ...context, response: responseStr, guardrail_response: responseStr };
4014
+ const exprResult = evaluateExpression(data.guardrailRejectionExpression, exprContext);
4015
+ guardrailResult.rejected = Boolean(exprResult);
4016
+ }
4017
+ else if (data.guardrailPassExpression) {
3888
4018
  const exprContext = { ...context, guardrail_response: responseStr };
3889
4019
  const exprResult = evaluateExpression(data.guardrailPassExpression, exprContext);
3890
4020
  guardrailResult.rejected = !exprResult;
3891
4021
  }
3892
4022
  }
3893
4023
  }
3894
- // Apply score threshold ONLY if no passExpression is configured
3895
- // This ensures passExpression takes precedence when both are set
3896
- if (!data.guardrailPassExpression && data.guardrailScoreThreshold !== undefined && guardrailResult.score !== undefined) {
4024
+ // Apply score threshold ONLY if no passExpression/rejectionExpression/fieldCheck is configured
4025
+ // This ensures explicit conditions take precedence
4026
+ if (!data.guardrailRejectionExpression && !data.guardrailPassExpression && !data.guardrailRejectionField &&
4027
+ data.guardrailScoreThreshold !== undefined && guardrailResult.score !== undefined) {
3897
4028
  guardrailResult.rejected = guardrailResult.score < data.guardrailScoreThreshold;
3898
4029
  }
3899
4030
  // Emit afterGuardrail checkpoint
@@ -3905,26 +4036,54 @@ Analyze the input above. Return a JSON object:
3905
4036
  return { cancelled: true, stage: 'afterGuardrail', guardrailResult };
3906
4037
  }
3907
4038
  if (guardrailResult.rejected) {
4039
+ const failAction = data.guardrailFailAction || 'error';
4040
+ const rejectMessage = data.guardrailCustomRejectMessage || 'Input rejected by guardrail';
3908
4041
  addTraceEntry(trace, {
3909
4042
  type: 'debug_step',
3910
4043
  nodeId: node.id,
3911
4044
  nodeName: data.label,
3912
- message: 'Guardrail rejected input',
3913
- data: { guardrailResult },
4045
+ message: `Guardrail rejected input, failAction=${failAction}`,
4046
+ data: { guardrailResult, rejectMessage },
3914
4047
  }, options);
3915
- return {
3916
- rejected: true,
3917
- guardrailResult,
3918
- input: currentInput,
3919
- };
4048
+ if (failAction === 'error' || failAction === 'stop') {
4049
+ // Return rejection result for branching (original behavior)
4050
+ return {
4051
+ rejected: true,
4052
+ guardrailResult,
4053
+ input: currentInput,
4054
+ };
4055
+ }
4056
+ else if (failAction === 'continue') {
4057
+ // Continue with reject message as the input
4058
+ currentInput = rejectMessage;
4059
+ addTraceEntry(trace, {
4060
+ type: 'debug_step',
4061
+ nodeId: node.id,
4062
+ nodeName: data.label,
4063
+ message: 'Continuing with reject message as input',
4064
+ }, options);
4065
+ }
4066
+ }
4067
+ else {
4068
+ // Guardrail passed - handle outputMode
4069
+ const outputMode = data.guardrailOutputMode || 'passthrough';
4070
+ addTraceEntry(trace, {
4071
+ type: 'debug_step',
4072
+ nodeId: node.id,
4073
+ nodeName: data.label,
4074
+ message: `Guardrail passed, outputMode=${outputMode}`,
4075
+ data: { score: guardrailResult.score },
4076
+ }, options);
4077
+ if (outputMode === 'original') {
4078
+ // Keep currentInput as-is (original input that was validated)
4079
+ // No change needed
4080
+ }
4081
+ else if (outputMode === 'reject-message') {
4082
+ // Use custom message even on pass
4083
+ currentInput = data.guardrailCustomRejectMessage || currentInput;
4084
+ }
4085
+ // 'passthrough' - keep currentInput as-is (default)
3920
4086
  }
3921
- addTraceEntry(trace, {
3922
- type: 'debug_step',
3923
- nodeId: node.id,
3924
- nodeName: data.label,
3925
- message: 'Guardrail passed',
3926
- data: { score: guardrailResult.score },
3927
- }, options);
3928
4087
  }
3929
4088
  catch (error) {
3930
4089
  addTraceEntry(trace, {