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:
self._update_function_invocation_continuation_state(...)
They settle dangling calls only when _process_model_function_calls(...) raises MiddlewareFailure:
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
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.
Description
With
terminate_on_unknown_calls=True, an unknown local function call raisesKeyErrorafter 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
mainat2c49f50cf08ebb6c1687146336f039051f159333.Root cause
Both non-streaming and streaming loops update continuation state immediately after the model response:
They settle dangling calls only when
_process_model_function_calls(...)raisesMiddlewareFailure:Unknown-call termination is raised separately as a plain
KeyErrorinside_try_execute_function_call_groups(...), so it bypasses that settlement path.Reproduction
Observed:
The same missing settlement boundary exists in the streaming loop.
Expected behavior
The configured fail-closed
KeyErrorshould 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 forMiddlewareFailure.Sessions without a service-managed continuation should not incur an extra model request.
Implementation considerations
A focused fix should:
KeyErrorexceptions;KeyErrorbehavior;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
maincommit.