Repository navigation
Description
Description
A process step that has a function without inputs fails when a message fills only part of another function's inputs.
Take a step with add(a, b) and reset(). A message that fills a makes the step raise ProcessTargetFunctionNameMismatchException. The process stops and add never runs. The same step without reset() waits for b and then runs add, as it should.
The Dapr runtime has the same code and the same problem.
Reproduction
I ran this on current main (df1cb33), Python 3.13. The event go fills add.a right away. It reaches add.b one step later, through a relay step.
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.local_runtime.local_event import KernelProcessEvent
from semantic_kernel.processes.local_runtime.local_kernel_process import start
from semantic_kernel.processes.process_builder import ProcessBuilder
class Relay(KernelProcessStep):
@kernel_function
async def pass_on(self, value: str) -> str:
return value
class AddOnly(KernelProcessStep):
@kernel_function
async def add(self, a: str, b: str) -> str:
print(" add called with", a, b)
return a + b
class AddAndReset(KernelProcessStep):
@kernel_function
async def add(self, a: str, b: str) -> str:
print(" add called with", a, b)
return a + b
@kernel_function
async def reset(self) -> str:
print(" reset called")
return "reset"
async def run(step_type):
builder = ProcessBuilder(name="fan_in")
relay = builder.add_step(Relay)
step = builder.add_step(step_type)
builder.on_input_event("go").send_event_to(step, function_name="add", parameter_name="a")
builder.on_input_event("go").send_event_to(relay, function_name="pass_on", parameter_name="value")
relay.on_function_result("pass_on").send_event_to(step, function_name="add", parameter_name="b")
await start(process=builder.build(), kernel=Kernel(), initial_event=KernelProcessEvent(id="go", data="x"))
for step_type in (AddOnly, AddAndReset):
print(step_type.__name__)
try:
asyncio.run(run(step_type))
print(" finished")
except Exception as ex:
print(" raised", type(ex).__name__ + ":", ex)Output on main:
An error occurred while running the process: A message targeting function `add` has resulted in a different function `reset` becoming invocable. Check the function names..
AddOnly
add called with x x
finished
AddAndReset
raised ProcessTargetFunctionNameMismatchException: A message targeting function `add` has resulted in a different function `reset` becoming invocable. Check the function names.
The step names and the event wiring are the same in both runs. Only the extra reset() function changes the result.
Cause
In LocalStep.handle_message (python/semantic_kernel/processes/local_runtime/local_step.py), the step stores the message values. Then it builds invocable_functions from every function whose inputs are all set.
A function with no required input has {} as its inputs, so it is always in that list. When the message targets add and add is not complete yet, target_function is None. Because reset is in the list, the code raises the mismatch error instead of waiting.
A function whose only parameter is KernelProcessStepContext is always in the list too, because that input is filled when the step starts.
StepActor.handle_message in python/semantic_kernel/processes/dapr_runtime/actors/step_actor.py has the same block.
Expected
A message can only change the function it targets. If that function still misses inputs, the step should log the missing keys and wait. This is what it already does for a step that has no other function.
Possible fix
I have a branch ready: https://github.com/VANDRANKI/semantic-kernel/tree/fix/process-step-partial-input-with-ready-function
It checks only the targeted function for missing inputs, in both LocalStep and StepActor. It adds 5 tests: 2 local step cases (a function with no inputs, and a function with only the step context), 1 Dapr step actor case, 1 end to end process like the script above, and 1 control that runs a no-input function when a message targets it. The 4 new tests that cover the bug fail on main and all 5 pass on the branch. The 239 existing process unit tests still pass.
I can't open a pull request from this account right now, so the compare link is main...VANDRANKI:semantic-kernel:fix/process-step-partial-input-with-ready-function
Activity