Fix async tool handling for compatibility with all LLMs.
This commit is contained in:
@@ -1918,12 +1918,16 @@ class FunctionCallInProgressFrame(ControlFrame, UninterruptibleFrame):
|
|||||||
tool_call_id: Unique identifier for this function call.
|
tool_call_id: Unique identifier for this function call.
|
||||||
arguments: Arguments passed to the function.
|
arguments: Arguments passed to the function.
|
||||||
cancel_on_interruption: Whether to cancel this call if interrupted.
|
cancel_on_interruption: Whether to cancel this call if interrupted.
|
||||||
|
is_async: Whether this function call runs asynchronously. When True,
|
||||||
|
the LLM continues the conversation immediately without waiting for
|
||||||
|
the result. The result is injected later via a developer message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
function_name: str
|
function_name: str
|
||||||
tool_call_id: str
|
tool_call_id: str
|
||||||
arguments: Any
|
arguments: Any
|
||||||
cancel_on_interruption: bool = False
|
cancel_on_interruption: bool = False
|
||||||
|
is_async: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -1067,16 +1067,25 @@ class LLMAssistantAggregator(LLMContextAggregator):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
in_progress_frame = self._function_calls_in_progress[frame.tool_call_id]
|
||||||
|
is_async = in_progress_frame.is_async if in_progress_frame else False
|
||||||
del self._function_calls_in_progress[frame.tool_call_id]
|
del self._function_calls_in_progress[frame.tool_call_id]
|
||||||
|
|
||||||
properties = frame.properties
|
properties = frame.properties
|
||||||
|
|
||||||
# Update context with the function call result
|
result = json.dumps(frame.result, ensure_ascii=False) if frame.result else "COMPLETED"
|
||||||
if frame.result:
|
|
||||||
result = json.dumps(frame.result, ensure_ascii=False)
|
if is_async:
|
||||||
self._update_function_call_result(frame.function_name, frame.tool_call_id, result)
|
# For async function calls instead of updating the existing IN_PROGRESS tool message we inject
|
||||||
|
# a new developer message so the LLM is notified of the completed result.
|
||||||
|
self._context.add_message(
|
||||||
|
{
|
||||||
|
"role": "developer",
|
||||||
|
"content": f"Async function with id '{frame.tool_call_id}' completed with result: {result}",
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._update_function_call_result(frame.function_name, frame.tool_call_id, "COMPLETED")
|
self._update_function_call_result(frame.function_name, frame.tool_call_id, result)
|
||||||
|
|
||||||
run_llm = False
|
run_llm = False
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ class FunctionCallRegistryItem:
|
|||||||
function_name: The name of the function (None for catch-all handler).
|
function_name: The name of the function (None for catch-all handler).
|
||||||
handler: The handler for processing function call parameters.
|
handler: The handler for processing function call parameters.
|
||||||
cancel_on_interruption: Whether to cancel the call on interruption.
|
cancel_on_interruption: Whether to cancel the call on interruption.
|
||||||
|
is_async: Whether this function call runs asynchronously. When True,
|
||||||
|
the LLM continues the conversation immediately without waiting for
|
||||||
|
the result. The result is injected later via a developer message.
|
||||||
timeout_secs: Optional per-tool timeout in seconds. Overrides the global
|
timeout_secs: Optional per-tool timeout in seconds. Overrides the global
|
||||||
``function_call_timeout_secs`` for this specific function.
|
``function_call_timeout_secs`` for this specific function.
|
||||||
"""
|
"""
|
||||||
@@ -131,6 +134,7 @@ class FunctionCallRegistryItem:
|
|||||||
function_name: Optional[str]
|
function_name: Optional[str]
|
||||||
handler: FunctionCallHandler | "DirectFunctionWrapper"
|
handler: FunctionCallHandler | "DirectFunctionWrapper"
|
||||||
cancel_on_interruption: bool
|
cancel_on_interruption: bool
|
||||||
|
is_async: bool = False
|
||||||
timeout_secs: Optional[float] = None
|
timeout_secs: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -578,6 +582,7 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
|
|||||||
handler: Any,
|
handler: Any,
|
||||||
*,
|
*,
|
||||||
cancel_on_interruption: bool = True,
|
cancel_on_interruption: bool = True,
|
||||||
|
is_async: bool = False,
|
||||||
timeout_secs: Optional[float] = None,
|
timeout_secs: Optional[float] = None,
|
||||||
):
|
):
|
||||||
"""Register a function handler for LLM function calls.
|
"""Register a function handler for LLM function calls.
|
||||||
@@ -589,6 +594,10 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
|
|||||||
parameter.
|
parameter.
|
||||||
cancel_on_interruption: Whether to cancel this function call when an
|
cancel_on_interruption: Whether to cancel this function call when an
|
||||||
interruption occurs. Defaults to True.
|
interruption occurs. Defaults to True.
|
||||||
|
is_async: Whether this function call runs asynchronously. When True,
|
||||||
|
the LLM continues the conversation immediately without waiting for
|
||||||
|
the result. The result is injected later via a developer message.
|
||||||
|
Defaults to False.
|
||||||
timeout_secs: Optional per-tool timeout in seconds. Overrides the global
|
timeout_secs: Optional per-tool timeout in seconds. Overrides the global
|
||||||
``function_call_timeout_secs`` for this specific function. Defaults to
|
``function_call_timeout_secs`` for this specific function. Defaults to
|
||||||
None, which uses the global timeout.
|
None, which uses the global timeout.
|
||||||
@@ -599,6 +608,7 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
|
|||||||
function_name=function_name,
|
function_name=function_name,
|
||||||
handler=handler,
|
handler=handler,
|
||||||
cancel_on_interruption=cancel_on_interruption,
|
cancel_on_interruption=cancel_on_interruption,
|
||||||
|
is_async=is_async,
|
||||||
timeout_secs=timeout_secs,
|
timeout_secs=timeout_secs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -607,6 +617,7 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
|
|||||||
handler: DirectFunction,
|
handler: DirectFunction,
|
||||||
*,
|
*,
|
||||||
cancel_on_interruption: bool = True,
|
cancel_on_interruption: bool = True,
|
||||||
|
is_async: bool = False,
|
||||||
timeout_secs: Optional[float] = None,
|
timeout_secs: Optional[float] = None,
|
||||||
):
|
):
|
||||||
"""Register a direct function handler for LLM function calls.
|
"""Register a direct function handler for LLM function calls.
|
||||||
@@ -619,6 +630,10 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
|
|||||||
handler: The direct function to register. Must follow DirectFunction protocol.
|
handler: The direct function to register. Must follow DirectFunction protocol.
|
||||||
cancel_on_interruption: Whether to cancel this function call when an
|
cancel_on_interruption: Whether to cancel this function call when an
|
||||||
interruption occurs. Defaults to True.
|
interruption occurs. Defaults to True.
|
||||||
|
is_async: Whether this function call runs asynchronously. When True,
|
||||||
|
the LLM continues the conversation immediately without waiting for
|
||||||
|
the result. The result is injected later via a developer message.
|
||||||
|
Defaults to False.
|
||||||
timeout_secs: Optional per-tool timeout in seconds. Overrides the global
|
timeout_secs: Optional per-tool timeout in seconds. Overrides the global
|
||||||
``function_call_timeout_secs`` for this specific function. Defaults to
|
``function_call_timeout_secs`` for this specific function. Defaults to
|
||||||
None, which uses the global timeout.
|
None, which uses the global timeout.
|
||||||
@@ -628,6 +643,7 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
|
|||||||
function_name=wrapper.name,
|
function_name=wrapper.name,
|
||||||
handler=wrapper,
|
handler=wrapper,
|
||||||
cancel_on_interruption=cancel_on_interruption,
|
cancel_on_interruption=cancel_on_interruption,
|
||||||
|
is_async=is_async,
|
||||||
timeout_secs=timeout_secs,
|
timeout_secs=timeout_secs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -766,6 +782,7 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
|
|||||||
tool_call_id=runner_item.tool_call_id,
|
tool_call_id=runner_item.tool_call_id,
|
||||||
arguments=runner_item.arguments,
|
arguments=runner_item.arguments,
|
||||||
cancel_on_interruption=item.cancel_on_interruption,
|
cancel_on_interruption=item.cancel_on_interruption,
|
||||||
|
is_async=item.is_async,
|
||||||
)
|
)
|
||||||
|
|
||||||
timeout_task: Optional[asyncio.Task] = None
|
timeout_task: Optional[asyncio.Task] = None
|
||||||
|
|||||||
@@ -389,9 +389,13 @@ class LLMContextSummarizationUtil:
|
|||||||
|
|
||||||
Scans messages from ``start_idx`` up to (but not including)
|
Scans messages from ``start_idx`` up to (but not including)
|
||||||
``summary_end`` to identify tool calls whose responses either don't
|
``summary_end`` to identify tool calls whose responses either don't
|
||||||
exist yet or fall in the kept portion of the context (>= summary_end).
|
exist yet, fall in the kept portion of the context (>= summary_end),
|
||||||
|
or are still marked as ``IN_PROGRESS`` (async calls whose results have
|
||||||
|
not yet arrived).
|
||||||
|
|
||||||
This prevents summarizing tool call requests when their responses would
|
This prevents summarizing tool call requests when their responses would
|
||||||
remain in the kept context as orphans, which the OpenAI API rejects.
|
remain in the kept context as orphans, which the OpenAI API rejects,
|
||||||
|
and avoids summarizing async function calls before their results arrive.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: List of messages to check.
|
messages: List of messages to check.
|
||||||
@@ -428,11 +432,13 @@ class LLMContextSummarizationUtil:
|
|||||||
if tool_call_id:
|
if tool_call_id:
|
||||||
pending_tool_calls[tool_call_id] = i
|
pending_tool_calls[tool_call_id] = i
|
||||||
|
|
||||||
# Check for tool results
|
# Check for tool results — treat IN_PROGRESS as still pending
|
||||||
|
# (async function calls whose results have not yet arrived).
|
||||||
if role == "tool":
|
if role == "tool":
|
||||||
tool_call_id = msg.get("tool_call_id")
|
tool_call_id = msg.get("tool_call_id")
|
||||||
if tool_call_id and tool_call_id in pending_tool_calls:
|
if tool_call_id and tool_call_id in pending_tool_calls:
|
||||||
pending_tool_calls.pop(tool_call_id)
|
if msg.get("content") != "IN_PROGRESS":
|
||||||
|
pending_tool_calls.pop(tool_call_id)
|
||||||
|
|
||||||
# If we have pending tool calls, return the earliest index
|
# If we have pending tool calls, return the earliest index
|
||||||
if pending_tool_calls:
|
if pending_tool_calls:
|
||||||
|
|||||||
Reference in New Issue
Block a user