Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ public List<GuardrailTaskResult> compileGuardrailTasks(
return new ArrayList<>();
}

String iterationRef = "${" + agentName + "_loop.iteration}";
// NOTE: the live loop counter lives under the DO_WHILE task's OUTPUT
// (Conductor exposes {input, output} per task ref), so it must be read as
// ${<loop>.output.iteration}. The bare ${<loop>.iteration} resolves to null
// mid-loop, which silently disabled every iteration-based escalation below.
String iterationRef = "${" + agentName + "_loop.output.iteration}";
return compileGuardrailTasksInternal(outputGuardrails, agentName, contentRef, iterationRef);
}

Expand Down Expand Up @@ -262,6 +266,10 @@ private GuardrailTaskResult compileCustomGuardrail(
normalizeInputs.put("worker_output", "${" + workerRef + ".output}");
normalizeInputs.put("guardrail_name", guard.getName());
normalizeInputs.put("default_on_fail", guard.getOnFail());
// Wire the live loop counter + retry budget so the normalize script can escalate
// retry -> raise once maxRetries is exhausted (parity with regex/llm guardrails).
normalizeInputs.put("iteration", iterationRef);
normalizeInputs.put("max_retries", guard.getMaxRetries());
normalizeTask.setInputParameters(normalizeInputs);

return new GuardrailTaskResult(List.of(task, normalizeTask), refName, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,7 @@ private WorkflowDef compileRotation(AgentConfig config, boolean random) {
Map<String, Object> selectInputs = new LinkedHashMap<>();
selectInputs.put("evaluatorType", "graaljs");
selectInputs.put("expression", selectScript);
selectInputs.put("iteration", ref(loopRef + ".iteration"));
selectInputs.put("iteration", ref(loopRef + ".output.iteration"));
if (config.getAllowedTransitions() != null) {
selectInputs.put("last_agent", "${workflow.variables.last_agent}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static WorkflowTask compileTermination(TerminationConfig config, String a
String taskName = agentName + "_termination";
String refName = agentName + "_termination";
String resultRef = "${" + llmRef + ".output.result}";
String iterationRef = "${" + agentName + "_loop.iteration}";
String iterationRef = "${" + agentName + "_loop.output.iteration}";

// Match local compiler: emit a SIMPLE worker task.
// The Python runtime registers a worker that evaluates the termination condition.
Expand Down Expand Up @@ -69,7 +69,7 @@ public static WorkflowTask compileStopWhen(String taskName, String agentName, St
agentName = AgentCompiler.toRef(agentName);
String refName = agentName + "_stop_when";
String resultRef = "${" + llmRef + ".output.result}";
String iterationRef = "${" + agentName + "_loop.iteration}";
String iterationRef = "${" + agentName + "_loop.output.iteration}";

WorkflowTask task = new WorkflowTask();
task.setName(taskName);
Expand Down Expand Up @@ -106,7 +106,7 @@ public static WorkflowTask compileStopWhenForConversation(String taskName, Strin

Map<String, Object> inputs = new LinkedHashMap<>();
inputs.put("result", "${workflow.variables.conversation}");
inputs.put("iteration", "${" + loopRef + ".iteration}");
inputs.put("iteration", "${" + loopRef + ".output.iteration}");
task.setInputParameters(inputs);

return task;
Expand Down Expand Up @@ -134,7 +134,7 @@ public static WorkflowTask compileTerminationForConversation(

Map<String, Object> inputs = new LinkedHashMap<>();
inputs.put("result", "${workflow.variables.conversation}");
inputs.put("iteration", "${" + loopRef + ".iteration}");
inputs.put("iteration", "${" + loopRef + ".output.iteration}");
task.setInputParameters(inputs);

return task;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,13 @@ public static String guardrailFixScript() {
public static String customGuardrailNormalizeScript() {
return iife(" var raw = $.worker_output;" + " var guardrailName = $.guardrail_name || 'guardrail';"
+ " var defaultOnFail = $.default_on_fail || 'retry';"
+ " var iteration = $.iteration || 0;"
+ " var max_retries = $.max_retries || 0;"
+ " function escalate(of, fixedOutput) {"
+ " if (of === 'retry' && iteration >= max_retries) return 'raise';"
+ " if (of === 'fix' && (fixedOutput === null || fixedOutput === undefined)) return 'raise';"
+ " return of;"
+ " }"
+ " if (raw == null) {"
+ " return {passed: true, message: '', on_fail: null,"
+ " fixed_output: null, guardrail_name: guardrailName,"
Expand All @@ -449,9 +456,10 @@ public static String customGuardrailNormalizeScript() {
+ " var existingOnFail = raw.on_fail !== undefined ? raw.on_fail : raw.onFail;"
+ " var fixedOutput = raw.fixed_output !== undefined ? raw.fixed_output : raw.fixedOutput;"
+ " var passed = raw.passed !== false && (existingOnFail == null || existingOnFail === 'pass');"
+ " return {passed: passed, message: raw.message || '', on_fail: existingOnFail,"
+ " var actualOnFail = passed ? existingOnFail : escalate(existingOnFail, fixedOutput);"
+ " return {passed: passed, message: raw.message || '', on_fail: actualOnFail,"
+ " fixed_output: fixedOutput, guardrail_name: raw.guardrail_name || raw.guardrailName || guardrailName,"
+ " should_continue: existingOnFail === 'retry'};"
+ " should_continue: actualOnFail === 'retry'};"
+ " }"
+ " if (raw != null && typeof raw === 'object'"
+ " && (raw.tripwire_triggered !== undefined || raw.tripwireTriggered !== undefined"
Expand All @@ -469,9 +477,10 @@ public static String customGuardrailNormalizeScript() {
+ " fixed_output: null, guardrail_name: guardrailName,"
+ " should_continue: false};"
+ " }"
+ " var tripwireOnFail = escalate(defaultOnFail, null);"
+ " return {passed: false, message: reason || (guardrailName + ' triggered'),"
+ " on_fail: defaultOnFail, fixed_output: null,"
+ " guardrail_name: guardrailName, should_continue: defaultOnFail === 'retry'};"
+ " on_fail: tripwireOnFail, fixed_output: null,"
+ " guardrail_name: guardrailName, should_continue: tripwireOnFail === 'retry'};"
+ " }"
+ " return {passed: true, message: '', on_fail: null,"
+ " fixed_output: null, guardrail_name: guardrailName,"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,39 @@ void testCustomGuardrail() {
assertThat(results.get(0).getTasks().get(1).getType()).isEqualTo("INLINE");
}

@Test
void testCustomGuardrailNormalizeWiresLiveIterationAndMaxRetriesForEscalation() {
GuardrailConfig g = GuardrailConfig.builder()
.name("custom_check")
.guardrailType("custom")
.position("output")
.taskName("my_guardrail_worker")
.onFail("retry")
.maxRetries(3)
.build();

GuardrailCompiler gc = new GuardrailCompiler();
var results = gc.compileGuardrailTasks(List.of(g), "agent", "${ref}");

WorkflowTask normalize = results.get(0).getTasks().get(1);
assertThat(normalize.getType()).isEqualTo("INLINE");
// The live loop counter must be read from the DO_WHILE task's OUTPUT
// (${<loop>.output.iteration}); the bare ${<loop>.iteration} resolves to null
// mid-loop and silently disables retry->raise escalation.
assertThat((String) normalize.getInputParameters().get("iteration"))
.isEqualTo("${agent_loop.output.iteration}");
assertThat(normalize.getInputParameters().get("max_retries")).isEqualTo(3);
// The normalize script must apply the same retry->raise coercion the regex/llm
// scripts use once the retry budget is exhausted.
assertThat((String) normalize.getInputParameters().get("expression"))
.contains("iteration >= max_retries")
.contains("'raise'")
// fix -> raise must be conditional on there being NO fixed output, otherwise
// a custom `fix` guardrail with a fixedOutput would be wrongly terminated
// instead of applying the fix (regression guard).
.contains("fixedOutput === null");
}

@Test
void testCustomGuardrailIncludesOpenAICompatibleInputAliases() {
GuardrailConfig g = GuardrailConfig.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ void testStopWhen() {
assertThat(task.getTaskReferenceName()).isEqualTo("agent_stop_when");
// Inputs bind to LLM result, loop iteration, and messages (stop_when needs conversation history)
assertThat((String) task.getInputParameters().get("result")).contains("agent_llm.output.result");
assertThat((String) task.getInputParameters().get("iteration")).contains("agent_loop.iteration");
assertThat((String) task.getInputParameters().get("iteration")).contains("agent_loop.output.iteration");
assertThat((String) task.getInputParameters().get("messages")).contains("agent_llm.input.messages");
}

Expand Down
Loading