{"record":{"id":"3a231e74205b2022","repo":"run-llama/llama_index","slug":"response-must-be-provided-to-stop-function","errorCode":null,"errorMessage":"Response must be provided to stop function.","messagePattern":"Response must be provided to stop function\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"llama-index-core/llama_index/core/query_engine/multistep_query_engine.py","lineNumber":21,"sourceCode":"from llama_index.core.base.base_query_engine import BaseQueryEngine\nfrom llama_index.core.base.response.schema import RESPONSE_TYPE\nfrom llama_index.core.callbacks.schema import CBEventType, EventPayload\nfrom llama_index.core.indices.query.query_transform.base import (\n    StepDecomposeQueryTransform,\n)\nfrom llama_index.core.prompts.mixin import PromptMixinType\nfrom llama_index.core.response_synthesizers import (\n    BaseSynthesizer,\n    get_response_synthesizer,\n)\nfrom llama_index.core.schema import NodeWithScore, QueryBundle, TextNode\n\n\ndef default_stop_fn(stop_dict: Dict) -> bool:\n    \"\"\"Stop function for multi-step query combiner.\"\"\"\n    query_bundle = cast(QueryBundle, stop_dict.get(\"query_bundle\"))\n    if query_bundle is None:\n        raise ValueError(\"Response must be provided to stop function.\")\n\n    return \"none\" in query_bundle.query_str.lower()\n\n\nclass MultiStepQueryEngine(BaseQueryEngine):\n    \"\"\"\n    Multi-step query engine.\n\n    This query engine can operate over an existing base query engine,\n    along with the multi-step query transform.\n\n    Args:\n        query_engine (BaseQueryEngine): A BaseQueryEngine object.\n        query_transform (StepDecomposeQueryTransform): A StepDecomposeQueryTransform\n            object.\n        response_synthesizer (Optional[BaseSynthesizer]): A BaseSynthesizer\n            object.\n        num_steps (Optional[int]): Number of steps to run the multi-step query.","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/run-llama/llama_index/blob/afd0fef371831f9bda13e5af7167cf4e981278ab/llama-index-core/llama_index/core/query_engine/multistep_query_engine.py#L3-L39","documentation":"default_stop_fn in multistep_query_engine.py reads stop_dict['query_bundle'] and raises if it is missing. The message text ('Response must be provided') is misleading — it actually means the MultiStepQueryEngine loop invoked the stop function with a dict that lacks the 'query_bundle' key (cast(None)). The stock loop always supplies it, so this almost always comes from a custom stop_fn that forwards an incomplete dict.","triggerScenarios":"Passing stop_fn to MultiStepQueryEngine where the callable expects a different dict shape (e.g. {'new_query_cb': ...} only), or calling a stop function copied from llama-index internals with a hand-built dict that omits 'query_bundle'. The built-in default_stop_fn itself never self-triggers because the engine's step loop passes {'query_bundle': qb, 'new_query_cb': cb}.","commonSituations":"Writing a custom early-stopping predicate based on outdated examples that used a different stop_dict schema; refactors that changed what the loop puts into the dict; version drift after the stop-function interface changed (there is a TODO in-source acknowledging the interface is rough).","solutions":["Make your custom stop_fn read stop_dict.get('query_bundle') and handle it defensively — mirror the stock signature shown in default_stop_fn.","If you just need the default behavior ('none' in the query string), omit stop_fn entirely and rely on default_stop_fn.","Fix call sites that build stop dicts manually to always include the 'query_bundle' key with a QueryBundle value.","For deterministic runs, prefer num_steps + early_stopping=False instead of a stop predicate — a simpler contract."],"exampleFix":"# before\ndef my_stop(stop_dict):\n    qb = stop_dict[\"query\"]          # wrong key -> KeyError path / None -> ValueError\n    return \"enough\" in qb\nengine = MultiStepQueryEngine(..., stop_fn=my_stop)\n\n# after\ndef my_stop(stop_dict):\n    qb = stop_dict.get(\"query_bundle\")   # exact key the loop supplies\n    if qb is None:\n        return True                      # be defensive instead of raising\n    return \"enough\" in qb.query_str.lower()\nengine = MultiStepQueryEngine(..., stop_fn=my_stop)","handlingStrategy":"validation","validationCode":"from llama_index.core.query_engine.multistep_query_engine import default_stop_fn\nfrom llama_index.core.schema import QueryBundle\n\nSTOP_DICT_KEYS = {\"query_bundle\", \"new_query_cb\"}\n\ndef safe_stop_fn(stop_dict: dict) -> bool:\n    qb = stop_dict.get(\"query_bundle\")\n    if not isinstance(qb, QueryBundle):\n        return True  # stop rather than crash on a malformed dict\n    return \"none\" in qb.query_str.lower()","typeGuard":"from llama_index.core.schema import QueryBundle\n\ndef is_valid_stop_dict(stop_dict: dict) -> bool:\n    return isinstance(stop_dict.get(\"query_bundle\"), QueryBundle)","tryCatchPattern":null,"preventionTips":["Model custom stop functions on the stock default_stop_fn signature exactly ({'query_bundle', 'new_query_cb'}).","Use .get() plus isinstance checks inside stop functions instead of indexing the dict.","Prefer num_steps with early_stopping=False when the stop contract feels brittle."],"tags":["llama-index","multi-step","stop-function","api-contract","misleading-message"],"backgroundTag":null,"analyzedSha":"afd0fef371831f9bda13e5af7167cf4e981278ab","analyzedAt":"2026-08-15T05:42:58.429Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}