Carry the parent's global inputs into an iterator or loop subflow
A container subflow is its own execution, so the global inputs typed on the
parent have to be handed to it. createAndStartSubflowChild took a "global."
view of getExecutionVariables(), but that is the *unprefixed* variables map -
the prefixed keys live in runtimeExecutionVariables, which is deliberately not
exposed. The view therefore always matched nothing, and every child was created
with null values for the globals its blocks reference.
The child then never left CREATED, and the parent failed with
Execution with id <child> is not in READY status (CURRENT STATUS is CREATED)
naming a child execution the user never sees and saying nothing about the cause.
GenericContainer was unaffected: its executor has its own copy of this
propagation that reads the runtime map, which is why only iterator and loop
subflows were broken.
The fix reads the parent's global inputs from the map that holds them.
The test runs an iterator whose subflow prompt references ${{global.who}}, and
fails with exactly the error above when the line is reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e69e21197
commit
4fa87ae3a2
|
|
@ -1060,7 +1060,12 @@ public class ExecutionsService {
|
|||
}
|
||||
}
|
||||
setExecutionVariableDescriptors(child.getId(), parent.getContext().getExecutionVariableDescriptors());
|
||||
Map<String, Object> globalInputs = ExecutionRuntimeContextSupport.globalView(parent.getContext().getExecutionVariables());
|
||||
// The parent's own global inputs, read from the map that holds them. This used to take a
|
||||
// "global." view of getExecutionVariables(), which is the *unprefixed* variables map and so
|
||||
// never matched the prefix: every child was handed null values, sat in CREATED and failed
|
||||
// the parent with "is not in READY status". The prefixed keys live in the runtime map,
|
||||
// which is deliberately not exposed - so go to the source instead.
|
||||
Map<String, Object> globalInputs = new LinkedHashMap<>(parent.getContext().getGlobalInputs());
|
||||
Map<String, ExecutionVariableDescriptor> globalDescriptors = new LinkedHashMap<>();
|
||||
for (IODescriptor required : child.getRequiredGlobalInputs()) {
|
||||
globalDescriptors.put(required.getName(), ExecutionVariableDescriptor.builder()
|
||||
|
|
|
|||
|
|
@ -1381,6 +1381,58 @@ public class ExecutionTest {
|
|||
&& event.getDetails().get("innerExecutionId") != null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void iteratorSubflowInheritsTheGlobalInputsSetOnTheParent() {
|
||||
// A subflow is its own execution, so the values typed on the parent have to be carried into
|
||||
// it. When they are not, the child sits in CREATED and the parent fails with "is not in
|
||||
// READY status", naming a child execution the user never saw and explaining nothing.
|
||||
Block<LLMBlockType> internalBlock = llmBlockFactory.create(LLMBlockConfiguration.builder()
|
||||
.name("Internal LLM")
|
||||
.llmDescriptor(llmBrick)
|
||||
.prompt("Hello, ${{name}} from ${{global.who}}!")
|
||||
.build());
|
||||
|
||||
Container<IteratorContainerType> container = iteratorContainerFactory.create(IteratorContainerConfiguration.builder()
|
||||
.name("Iterator")
|
||||
.subFlow(FlowData.builder()
|
||||
.block(internalBlock)
|
||||
.globalInput(IODescriptor.input("who", IOType.TEXT, false, null))
|
||||
.build())
|
||||
.iterationInput("name")
|
||||
.build());
|
||||
|
||||
ExecutionObject execObject = executionsService.createExecution("Iterator flow", FlowData.builder()
|
||||
.container(container)
|
||||
.globalInput(IODescriptor.input("who", IOType.TEXT, false, null))
|
||||
.build());
|
||||
|
||||
// The parent collects what the subflow needs: it is the only place the user can supply it.
|
||||
assertTrue(execObject.getRequiredGlobalInputs().stream()
|
||||
.anyMatch(descriptor -> descriptor.getName().equals("who")));
|
||||
|
||||
executionsService.setGlobalInput(execObject.getId(), "who", "HR");
|
||||
executionsService.prepareInput(execObject.getId(), container.getId(), "name", List.of("Alice", "Bob"));
|
||||
assertEquals(ExecutionStatus.READY, execObject.getContext().getStatus());
|
||||
|
||||
execObject = executionsService.startExecution(execObject.getId());
|
||||
// WAITING is transient here: the parent suspends between iterations while a child runs.
|
||||
for (int attempt = 0; attempt < 200 && !execObject.getContext().getStatus().isFinalState(); attempt++) {
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
execObject = executionsService.getExecution(execObject.getId());
|
||||
}
|
||||
|
||||
ExecutionObject finished = execObject;
|
||||
assertEquals(ExecutionStatus.SUCCESS, finished.getContext().getStatus(),
|
||||
() -> "errors: " + finished.getContext().getErrors());
|
||||
assertEquals(List.of("Hello, Alice from HR!", "Hello, Bob from HR!"),
|
||||
finished.getContext().getResult().values().stream().findFirst().orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void genericContainerSuspendsForInnerInteractionAndResumesFromChild() {
|
||||
Block<HumanInteractionBlockType> innerInteraction = humanInteractiveBlockFactory.create(
|
||||
|
|
|
|||
Loading…
Reference in New Issue