Python: unknown-call termination leaves service-managed conversations unresolved

Author: CorgiBoyGCreated Sep 6, 2026Updated Sep 18, 2026
Labelspythonagentsreproduced

Description

With terminate_on_unknown_calls=True, an unknown local function call raises KeyError after a service-managed continuation has already been persisted, but the function-calling loop does not settle the unresolved server-side call before propagating the exception.

This leaves the session pointing at a hosted response whose function call has no terminal output. Response-ID and similar service-managed continuations can reject the next request over that session because the prior call remains unresolved.

The behavior is present on current main at 2c49f50cf08ebb6c1687146336f039051f159333.

Root cause

Both non-streaming and streaming loops update continuation state immediately after the model response:

python
self._update_function_invocation_continuation_state(...)

They settle dangling calls only when _process_model_function_calls(...) raises MiddlewareFailure:

python
try:
    function_processing = await _process_model_function_calls(...)
except MiddlewareFailure:
    await self._settle_dangling_service_function_calls(...)
    raise

Unknown-call termination is raised separately as a plain KeyError inside _try_execute_function_call_groups(...), so it bypasses that settlement path.

Reproduction

python
client = MockBaseChatClient()

@tool(name="known_func")
def known_func(value: str) -> str:
    return value

client.function_invocation_configuration["terminate_on_unknown_calls"] = True
client.run_responses = [
    ChatResponse(
        messages=Message(
            role="assistant",
            contents=[
                Content.from_function_call(
                    call_id="u1",
                    name="unknown_func",
                    arguments={},
                ),
                Content.from_function_call(
                    call_id="k1",
                    name="known_func",
                    arguments={"value": "x"},
                ),
            ],
        ),
        conversation_id="conv_123",
    )
]

session = AgentSession()
agent = Agent(client=client, tools=[known_func])

with pytest.raises(KeyError, match="unknown_func"):
    await agent.run("run", session=session)

assert session.service_session_id == "conv_123"
assert client.call_count == 2  # fails: actual value is 1

Observed:

exception=KeyError:'Error: Requested function "unknown_func" not found.'
conversation_id=conv_123
call_count=1

The same missing settlement boundary exists in the streaming loop.

Expected behavior

The configured fail-closed KeyError should still reach the caller, but a service-managed conversation must first receive one terminal error result per unresolved local function call, with tool calling disabled, just as it does for MiddlewareFailure.

Sessions without a service-managed continuation should not incur an extra model request.

Implementation considerations

A focused fix should:

  • distinguish unknown-call termination from unrelated KeyError exceptions;
  • settle the original unresolved batch in both streaming and non-streaming paths;
  • preserve the public KeyError behavior;
  • advance response-ID continuation state to the settlement response;
  • keep settlement best-effort so settlement failure never masks the original abort;
  • cover both service-managed and ordinary sessions.

This should remain separate from mixed-batch precedence work: the defect already reproduces with an ordinary unknown-call batch on main.

I can prepare a focused fix and regression coverage after the core team confirms the intended exception/settlement boundary.

AI assistance was used for call-path analysis and drafting. The reproduction above was executed against an isolated checkout of the cited main commit.

Source: microsoft/agent-framework