Skip to content

Python: Bug: stop_process() never stops a process, the edge is linked under the wrong key and the end step id is random #14730

Open

Description

Description

stop_process() does nothing in the Python process framework. A process that has a stop_process() edge keeps running supersteps after the stop event fires. It only ends when no messages are left, or at the superstep limit.

There are three problems that add up to this:

  1. ProcessStepEdgeBuilder.stop_process links the edge under the key "END". It should use the id of the event it was set on. No step ever emits an event called END, so the edge never fires.
  2. The end step gets a random id from ProcessStepBuilder.__init__. The runtimes look for END_PROCESS_ID. The class has an END_STEP_ID constant, but nothing uses it.
  3. In LocalProcess.internal_execute, an end message only does break out of the inner for message in messages_to_process loop. The outer superstep loop goes on.

Reproduction

I ran this on current main (df1cb33), Python 3.13. The checker step sends work to a worker and signals Done on every run. The worker sends the result back to the checker, so the process loops. Done is wired to stop_process().

import asyncio

from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.processes.const import END_PROCESS_ID
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
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

calls = []


class Checker(KernelProcessStep):
    @kernel_function
    async def check(self, value: str, context: KernelProcessStepContext) -> None:
        calls.append("check")
        await context.emit_event("Work", data=value)
        await context.emit_event("Done", data=None)


class Worker(KernelProcessStep):
    @kernel_function
    async def work(self, value: str) -> str:
        calls.append("work")
        return value


builder = ProcessBuilder(name="stopper")
checker = builder.add_step(Checker)
worker = builder.add_step(Worker)
builder.on_input_event("go").send_event_to(checker, function_name="check", parameter_name="value")
checker.on_event("Work").send_event_to(worker, function_name="work", parameter_name="value")
checker.on_event("Done").stop_process()
worker.on_function_result("work").send_event_to(checker, function_name="check", parameter_name="value")

process = builder.build()
checker_info = next(step for step in process.steps if step.state.name == "Checker")
for event_id, edges in checker_info.output_edges.items():
    for edge in edges:
        print("edge key:", event_id, "-> function", edge.output_target.function_name, "step_id", edge.output_target.step_id)
print("END_PROCESS_ID:", END_PROCESS_ID)

asyncio.run(start(process=process, kernel=Kernel(), initial_event=KernelProcessEvent(id="go", data="x")))
print("check calls:", calls.count("check"), "work calls:", calls.count("work"))

Output on main:

edge key: Checker_f392ac242d084276b01b79a80495429a.Work -> function work step_id f0cbe097573448c6badbc9e93d967e22
edge key: END -> function END step_id 381c1ac4a9d14f6b9d8ee1c251d9268a
END_PROCESS_ID: Microsoft.SemanticKernel.Process.EndStep
check calls: 50 work calls: 50

The Done event has no edge. The stop edge sits under END, and its step_id is a random id, not END_PROCESS_ID. The loop runs until the default limit of 100 supersteps.

Expected

The process finishes the superstep in which the stop event fired, and then ends. Here that is one check call and one work call.

Cause

process_step_edge_builder.py has self.source.link_to("END", self). The existing unit test test_stop_process asserts that exact call, so it passes with the wrong key.

Fixing only one of the three is not enough. I patched them one at a time on main and ran the script above:

  • With the right key only, the run fails with RuntimeError: coroutine raised StopIteration. The message has a random destination_id, so message.destination_id == END_PROCESS_ID is false. Then next(step for step in self.steps if step.id == message.destination_id) finds no step.
  • With the right key and the fixed end step id, the run still prints check calls: 50 work calls: 50. The break only leaves the inner message loop.

The Dapr runtime already expects the fixed id. ProcessActor._is_end_message_sent looks at the message buffer of ActorId(END_PROCESS_ID) at the start of every superstep. I could not run the Dapr runtime, so I did not test it.

Possible fix

I have a branch ready: https://github.com/VANDRANKI/semantic-kernel/tree/fix/process-stop-process-never-stops

It does three small things. stop_process links the edge under the event id. EndStep gets the fixed id END_STEP_ID, which equals END_PROCESS_ID. The local runtime remembers that an end message was sent, runs the rest of the superstep, and then leaves the loop. The same script prints check calls: 1 work calls: 1 on the branch.

It adds 4 tests in a new file, tests/unit/processes/local_runtime/test_local_process_stop.py. It also fixes test_stop_process, which asserted the wrong key. 3 of the new tests and the fixed test fail on main. The 4th new test is a control: a looping process without a stop edge keeps running up to max_supersteps. All 243 process unit tests pass on the branch.

I can't open a pull request from this account right now, so the compare link is main...VANDRANKI:semantic-kernel:fix/process-stop-process-never-stops

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonPull requests for the Python Semantic Kerneltriage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions